Mostrando postagens com marcador CiênciaEmPython. Mostrar todas as postagens
Mostrando postagens com marcador CiênciaEmPython. Mostrar todas as postagens

sexta-feira, 16 de junho de 2017

Relatórios e textos técnicos usando Pweave

Todos os textos do blog são gerados usando uma ferramenta chamada pweave, que permite escrever textos alternando markdown (ou HTML, Latex e reST) e código em Python. Apesar de ser parecido com o Jupyter Notebook, o foco do Pweave é na criação de relatórios e não na execução de código ao mesmo tempo que o texto é editado. Quando um documento é compilado pelo Pweave seu código é executado e um documento de saída é criado com os resultados. Isso facilita muito a edição dos textos, pois podemos usar nosso editor favorito ao invés da interface web do Jupyter (ela não me agrada, mas isto é bem pessoal...).

A instalação do Pweave pode ser feita via conda

conda config --add channels mpastell
conda install pweave

ou fazendo download do código a partir do PyPI. A página inicial do Pweave cite um problema com a instalaçõa via pip e Python 3. Não sei se este problema ainda existe, então pode valer a pena investigar.

Pweave pode ser utilizado de duas maneiras: convertendo um texto em markdown com código Python (usando o comando pweave) ou convertendo um script Python com comentários contendo texto em markdown (usando o comando pypublish). Ambos suportam as praticamente as mesmas funcionalidades, mas dependendo do uso um pode ser mais conveniente que o outro. Eu prefiro o primeiro modo para escrever os textos do blog.

Opções de entrada

Documentos são compostos por dois tipos de trechos (chunks): documentação e código. Trechos de documentação não são processados pelo Pweave e são copiados diretamente para a saída. Trechos de código (code chunks) são executados pelo Pweave e sua saída é colada no documento de resultado. É possível adicionar opções que controlam como a saída é processada. As que mais uso são:

  • fig=True: cola figuras do matplotlib no documento;
  • caption='': legenda de figuras;
  • source='': carregar o código deste chunk de um arquivo;
  • evaluate=False: somente mostrar o código, sem avaliar;
  • engine='python'|'shell': executar código em Python ou comandos no terminal.

Todas as opções de configuração de code chunks podem ser consultadas aqui.

Opções de saída

A transformação dos trechos de código no documento de saída depende do formato utilizado. Veja abaixo uma lista dos formatos mais comuns:

  • markdown: Produz texto em markdown na saída (em formato pandoc);
  • md2html: Produz documento HTML a partir de uma entrada em markdown (usando o módulo markdown);
  • pandoc2html: Markdown para HTML usando pandoc;
  • pandoc2latex Markdown para Latex usando pandoc;
  • rest: Documento em Restructured Text.

Mais formatos de saída em Latex podem ser vistos na documentação oficial). Também são suportados outros formatos mais obscuros.

Um ponto importante é que os formatos que exportam HTML suportam equações escritas no formato Latex usando MathJax. Logo, é possível criar textos bastante técnicos e exportar diretamente para HTML (além de Latex,é claro). Um exemplo é o texto que fiz sobre Regressão Logística.

Usando Pypublish

O comando pypublish recebe um arquivo em Python como entrada e devolve um documento formatado. Toda linha de comentário começando com #' é incluída como markdown no documento formatado. O restante do código é executado. Opções para a visualização do código podem ser definidas usando #+. Veja abaixo um exemplo de script que pode ser publicado usando pypublish.

#' % título
#'% author
#'% data

#'este texto é um exemplo em markdown para pweave.

print('hello!')
var = 7

#' O valor de var é <%= var %>!.

#' Abaixo um gráfico (com legenda!). O código usado para gerar o
gráfico não aparece no resultado final.

#+ caption='Gráfico com legenda.',echo=False
import numpy as np
import matplotlib.pyplot as plt
plt.plot(np.random.rand(10), '-')
plt.show()

#' E o texto acaba aqui.

O comando para publicar o script é

pypublish -f (html ou pdf) arquivo.py

Para saída em pdf é necessário ter instalado pandoc e pdflatex.

Usando pweave

O comando pweave recebe um arquivo texto com trechos em Python e o converte para um documento processado contendo os trechos de código e seu resultado. Figuras são embutidas direto na página. O código incluido nos code chunks é executado na mesma seção (ou seja, tudo que foi escrito em um trecho continua valendo nos próximos).

% título
% author
% data

este texto é um exemplo em markdown para pweave.

<<>>=
print('hello!')
var = 7
@

O valor de var é <%= var %>!.

Abaixo um gráfico (com legenda!). O código usado para gerar o gráfico
não aparece no resultado final.

<<caption='Gráfico com legenda.',echo=False>>=
import numpy as np
import matplotlib.pyplot as plt
plt.plot(np.random.rand(10), '-')
plt.show()
@

E o texto acaba aqui.

O comando usado para processar o arquivo acima é:

pweave -f (formato incluso na lista acima) arquivo

Lembre-se de que o arquivo de entrada deverá ser compatível com o formato escolhido. Um outro exemplo pode ser este texto mesmo, que foi compilado usando Pweave. Seu código fonte pode ser visto aqui.


O resultado gerado pelos exemplos acima pode ser visto aqui. Apesar de ambos comandos gerarem a mesma saída eles tem usos diferentes. Quando desejamos fazer um texto que contém exemplos e gráficos gerados usando Python é melhor usar o comando pweave. Por outro lado, se temos um código já pronto que precisa ser usado em outros scripts e queremos documentá-lo sem alterar sua funcionalidade podemos usar o pypublish.

Espero que este texto tenha sido útil. Tenho visto o Pweave como uma excelente alternativa ao Jupyter Notebook para a criação de textos técnicos, com a grande vantagem de que posso usar meu editor de texto favorito ao invés da interface Web do Jupyter. Qualquer dúvida ou comentário é só comentar. Se gostou, compartilhe e ajude o blog a ficar mais conhecido ;)

sexta-feira, 26 de maio de 2017

Medindo consumo de memória em Python

O gerenciamento de memória de programas em Python é feito inteiramente pela linguagem. Isto significa que, por vezes, pode ser difícil controlar o uso de memória e não costuma ser claro o quanto cada objeto ocupa na memória nem quando seu espaço é liberado. Descreverei neste post o uso do módulo memory_profiler, que contém ferramentas para medir o consumo de memória de scripts Python.

Antes de tudo, o memory_profiler pode ser instalado via pip usando o seguinte comando. A opção -U pode ser usada para instalar o módulo na área do usuário em sistemas Unix-like.

$ pip install memory_profiler

Para usar este módulo basta decorar a função que desejamos medir o consumo de memória com o decorador @profile e executar o programa usando

$ python -m memory_profiler arquivo.py

Este comando imprime um relatório linha a linha mostrando o consumo de memória. Quando objetos são criados o consumo de memória aumenta, enquanto quando eles saem de contexto o consumo pode diminuir. O ponto fraco deste modo de execução é que ele só mostra o consumo medido "entre" as linhas do programa. Ou seja, se uma função for chamada e, durante sua execução alocar e liberar uma grande quantidade de memória estes valores não são contabilizados.

Também é disponibilizado o comando mprof, que executa um script e mede o consumo total de memória em pequenos intervalos de tempo. mprof, porém, não discrima onde a memória está sendo usada.


Irei exemplificar o uso do mprof com dois exemplos tirados da TRIOSlib, a biblioteca que desenvolvi durante meu doutorado. O objetivo da biblioteca é usar aprendizado de máquina para tratar problemas de processamento de imagens (mais detalhes aqui).

Uma das tarefas que mais consome memória na TRIOSlib é a extração de características. Um padrão é extraído de cada ponto das imagens de treinamento. Conjuntos de treinamento contendo várias centenas de milhares de padrões são comuns, mesmo usando um pequeno número de imagens de treinamento.

A TRIOSlib implementa dois modos para extração de características. No primeiro uma matriz é alocada com uma linha por pixel das imagens de treinamento. Cada padrão observado é armazenado em uma linha e, se houverem repetições, o mesmo padrão é armazenado diversas vezes. Neste caso o consumo depende do número de pixels das imagens de treinamento. Nos referimos a este modo como modo Array.

No segundo modo a matriz é trocada por um dicionário cujas chaves são padrões observados nas imagens. Se houverem repetições não há consumo extra de memória. Neste caso o consumo de memória depende do número de padrões únicos nas imagens. Quanto maior a repetição de padrões menor será o consumo relativo ao primeiro modo. Nos referimos a este tipo de execução como modo Dicionário ou Dict.

Apesar do memory_profiler possuir um modo que mostra o consumo de memória linha a linha, para operações longas como o treinamento de operadores pode ser mais interessante usar o comando mprof. Apesar de não ser possível identificar qual estrutura está consumindo memória, podemos ver o quanto a memória cresce e diminui conforme o programa executa. Também conseguimos, por meio do decorador @profile, marcar o início e o fim da execução de funções no código.

Para executar um script usamos o comando mprof run arquivo.py. Podemos plotar um gráfico com a última execução usando mprof plot. Usaremos o código abaixo como exemplo.

import trios
import trios.feature_extractors
import trios.classifiers

import numpy as np
from sklearn.tree import DecisionTreeClassifier

import sys


@profile
def main(ordered=True):
    imgset =
trios.Imageset.read('/media/igor/Data1/datasets/staffs/level1.set')
    testset =
trios.Imageset.read('/media/igor/Data1/datasets/staffs/test-
small.set')
    win = np.ones((7, 7), np.uint8)

    wop = trios.WOperator(win,
trios.classifiers.SKClassifier(DecisionTreeClassifier(),
ordered=ordered), trios.feature_extractors.RAWFeatureExtractor(win))

    dataset = wop.extractor.extract_dataset(imgset, ordered=ordered)
    if ordered:
        print(dataset[1].shape[0])
    else:
        print(len(dataset))

    tr2 = profile(wop.classifier.train)
    tr2(dataset, {})
    wop.trained = True

    print(wop.eval(testset[:1]))

if __name__ == '__main__':
    main(sys.argv[1] == 'ordered')

O decorator @profile irá marcar no gráfico o início e fim das funções main e WOperator.Classifier.train. (Lembre-se que um decorador é simplesmente uma função que retorna outra função. Fazemos isto na linha 21 para obter uma função de treinamento com @profile.) A escolha do modo de extração de características é feita pelo parâmetro ordered, passado pela linha de comando. ordered=True implica no uso de uma matriz para os padrões. ordered=False usa um dicionário. Veja abaixo o consumo de memória medidos em uma execução usando os dois modos.

Fig 1: Modo Array

Fig 2: Modo Dict

O consumo máximo de memória do modo Array é cerca de 4 vezes maior do que o modo Dict! A principal diferença entre os dois modos de execução é a quantidade de memória usada pelo método train. Enquanto o modo Array executa a extração de características muito mais rápido que o modo Dict, o treinamento é muito mais custoso tanto em termos de tempo quanto em memória. Isto pode ser explicado pelo fato de que o modo Dict condensa todas as observações repetidas em uma só linha ao treinar. Isto resulta em uma matriz de treinamento com 137.453 linhas, contra 5.595.585 linhas quando usamos o modo Array. A redução no número de linhas é possível atribuindo ao peso de cada exemplo de treinamento único o número de repetições observadas nas imagens. Infelizmente a maioria dos classificadores do scikit-learn não oferece suporte a definir pesos para cada instância de treinamento. Classificadores baseados em árvores de decisão (encontrados no pacote sklearn.tree) possuem este suporte e, como podemos ver isto faz uma grande diferença ao trabalhar com dados em que há repetição nos padrões de entrada observados.


Neste texto exemplifiquei a utilização do memory_profiler para medição de consumo de memória em Python e do decorador @profile para marcar o início e o fim da execução de diversas funções. Como pudemos ver, utilizar estruturas adequadas pode diminuir significativamente o consumo de memória e permitir processar uma maior quantidade de dados em memória.

sexta-feira, 28 de abril de 2017

Configurando programas usando arquivos INI

Uma boa prática ao desenvolver programas é tornar algumas partes configuráveis pelo usuário. Isto pode ser feito usando arquivos de configuração em texto puro. Desta maneira, o usuário pode abrir o arquivo e entender seu conteúdo facilmente, além de poder fazer alterações controladas na configuração do programa. Em sistemas unix-like estes arquivos costumam ficar na pasta ~/.config do usuário. Em sistemas Windows arquivos com a extensão ini são usados e costumam ficar na pasta da aplicação.

Uma vantagem de se usar arquivos ini é que eles são de fácil leitura e podem ser lidos pela biblioteca configparser da biblioteca padrão do Python. Arquivos ini seguem a seguinte formatação:

[DEFAULT]
chave=valor
chave2=valor2
chave3 = valor3
; comentário em uma linha separada!
# outro comentário!

[Seção 2]
chave=valor_atualizado
; o restante dos valores é igual ao encontrado em DEFAULT

[Seção 3]
chave3=valor_novo3
chave2=valor_novo2
; o mesmo ocorre para esta seção.

A configuração possui várias Seções, cada uma representando uma configuração possível do programa, que contém diversas propriedades representadas como um par (chave, valor) em que ambas a chave e o valor podem conter espaços (assim como o nome das seções). Para evitar a repetição de valores padrão para cada propriedade, toda chave não definida em uma seção assume, automaticamente, o valor declarado na seção DEFAULT.

>>> import codecs
>>> from configparser import ConfigParser
>>> config = ConfigParser()
>>> with codecs.open('exemplo.ini', 'r', 'utf-8') as f:
...     config.readfp(f)
...
>>> config.sections()
['Seção 2', 'Seção 3']
>>> for k in config.sections():
...     print('Seção:', k)
...     for prop in config[k].keys():
...         print('\t',prop,':',config[k][prop])
...
Seção: Seção 2
         chave : valor_atualizado
         chave2 : valor2
         chave3 : valor3
Seção: Seção 3
         chave3 : valor_novo3
         chave2 : valor_novo2
         chave : valor

Veja que, como esperado, todas as seções possuem as mesmas 3 propriedades, mesmo que Seção 2 e Seção 3 tenham declarado somente parte delas.

Em um contexto de programação científica podemos usar arquivos ini para representar configurações de um experimento e grupos parâmetros a serem variados de maneira conjunta. Isto facilita também a reprodução de experimentos, já que os parâmetros usados estão descritos de forma clara fora do código fonte do programa. Não é necessário caçar nenhuma informação no meio do programa!

Veja abaixo um exemplo que faz detecção de objetos de uma certa cor usando scikit-image. A principal configuração do programa é qual a Matiz da cor a ser identificada. Ao invés de fixar um valor no código fonte ou criar arquivos de configuração em modo texto vamos criar um arquivo ini que permite ao usuário configurar diversos perfis para identificação de cores. Veja abaixo o arquivo que usaremos

[DEFAULT]
cor_h = 0

[vermelho]
cor_h = 0

[azul]
cor_h = 202

Rodaremos o programa de segmentação por cor na imagem abaixo. Cada configuração realiza a segmentação de uma das canetinhas da imagem.

Abaixo está a listagem de código que faz a segmentação por cor em duas imagens.

import sys
import skimage
import skimage.color
import skimage.io
import numpy as np
import matplotlib.pyplot as plt
from configparser import ConfigParser

def segmenta_cor(imagem, cor):
    imagem_hsv = skimage.color.rgb2hsv(imagem)
    cor_h = cor/360
    color_range = 0.05

    canal_h = imagem_hsv[:,:,0]
    canal_h_cor = np.logical_and(canal_h > (cor_h - 0.05), canal_h <
(cor_h + 0.05))
    if cor_h < 0.05:
        canal_h_cor = np.logical_or(canal_h_cor, canal_h > 1 - 0.05 +
cor_h)
    return 255*canal_h_cor

config = ConfigParser()
config.read('cores.ini')

if __name__ == '__main__':
    secname = sys.argv[1]
    if secname in config.sections():
        cor_h = int(config[secname]['cor_h'])
        img = skimage.io.imread('foto.jpg')
        msk = segmenta_cor(img, cor_h)
        skimage.io.imsave('mask_%s.png'%secname, msk)

Chamamos o programa com o nome da seção que contém a configuração desejada.

python segmenta_cores.py vermelho
python segmenta_cores.py azul

E cada execução realmente produz uma segmentação diferente da imagem:

Se quisermos segmentar a cor verde podemos simplesmente adicionar uma seção no arquivo cores.ini. Não há necessidade de mudar o código fonte!

Qualquer dúvida ou sugestão deixe um comentário abaixo ;) Se gostou, compartilhe. Até a próxima!