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, 9 de junho de 2017

Uma estratégia para começar a terminar os projetos que começamos

Como muitos outros programadores, eu tenho uma grande vontade de criar programas e aplicativos. Assim, vivo arranjando desculpas para começar algum projeto novo ou estudar uma nova linguagem. Passo um tempo considerável fazendo pesquisas sobre o assunto, procurando tecnologias "adequadas" (ou seja, que me agradem) e pensando como este programa seria. O problema é que dificilmente estes projetos alcançam uma primeira versão usável. Na verdade, muitas vezes eles não são nem iniciados e sempre acabam substituídos por outras ideias ou novidades. Mais ou menos assim.

Neste texto tentarei elaborar algumas ideias de como tento direcionar esta energia criativa de modo mais efetivo. Antes de tudo, eu ainda sofro deste problema e frequentemente me vejo pesquisando a fundo coisas que só seriam úteis em um eventual projeto que eu sei que não completarei. É verdade que isto me dá uma cultura geral (tecnológica) interessante, mas o custo benefício é bem baixo. São horas demais gastas em resultados de menos, por assim dizer. Porém, a técnica que descrevo neste texto me ajudou a transformar parte deste tempo gasto em algum resultado palpável.

Para mim, a melhor maneira que encontrei de direcionar essa vontade e energia criativa foi exercitá-la todos os dias ao invés de deixar tudo acumular e passar horas pesquisando. Como gosto também de escrever e, resolvi direcionar essa energia para escrever neste blog. Todos os dias escrevo por 30 minutos, de preferência de manhã, e procuro ter sempre um post pronto toda sexta-feira. Se tenho algo bom antes eu agendo para a próxima sexta e continuo a escrever. Se o assunto é extenso divido em várias partes e publico uma por vez. Já são 9 semanas com textos publicados regularmente e hoje estou no meu 17 dia de escrita seguido sem interrupção.

Um ponto essencial desta rotina é o tempo que reservo para escrever e o horário que o faço. Se eu deixo para escrever de noite existem grandes chances que eu estarei muito cansado ou que algo aconteça e eu não tenha tempo. Se eu simplesmente começar em qualquer momento do dia posso negligenciar outras tarefas mais importantes. Ou pode acontecer o contrário e eu nunca escrever pois outras tarefas são mais urgentes. Em geral, escrever se torna frustrante pois é uma tarefa que eu sei que gostaria de fazer mas acabo não conseguindo realizar a contento.

Essa estratégia está dividida em três componentes igualmente importantes: foco, hábito e objetivo.

Para exercitar o foco eu sempre escrevo usando um cronômetro. Assim que eu começo inicio a contagem e termino assim que ele alcançar os 30 minutos. O senso de urgência de ter pouco tempo para esta atividade aumenta minha concentração e faz com que este tempo seja reservado somente para a escrita. O objetivo é aprender a ligar o foco e dedicar um tempo para fazer uma atividade com 100% de concentração. É assustador o quanto conseguimos ser produtivos ao usar 100% da nossa atenção em uma só tarefa. Isto pode ser relaxante também. Muitas vezes estou menos cansado ao terminar do que antes de escrever.

O hábito é igualmente importante nesta estratégia. A regularidade evita que muita energia criativa acumule e eu resolva iniciar novos projetos e pesquisas. Gosto muito da seguinte citação, atribuída a William Falkner.

"I Only Write When Inspiration Strikes. Fortunately It Strikes at Nine Every Morning"

Além disto, a regularidade transforma uma atividade em um hábito. Da mesma maneira que todos os dias escovo os dentes ou tomo banho mais ou menos nos mesmos horário, tenho o hábito de escrever e não é necessário grande esforço para começar.

O último ponto importante é o objetivo. Ter foco e regularidade pode até ajudar a controlar nossa energia criativa e evitar que ela nos domine, mas colocar um objetivo transforma essa energia criativa em um benefício concreto para nós mesmos. Se, por exemplo, decido passar um tempo pesquisando sobre uma linguagem de programação ou tecnologia diferente, posso transformar esta busca em um tutorial ou um screencast. Ao final da busca tenho, além do conhecimento adquirido, um material escrito que demonstra meu conhecimento no assunto. Este resultado obtido pode ter o formato de contribuições para um software livre ou palestras. No meu caso, o objetivo é publicar um texto novo a cada sexta-feira. Este é o nono texto que publico seguido e me sinto mais motivado do que quando comecei.

Quando juntamos os três componentes, foco, hábito e objetivo, conseguimos domar nossa energia criativa e direcioná-la para nosso benefício. Apesar de ser somente uma pequena quantidade de tempo (30 minutos por dia), exercitar estes 3 componentes pode trazer benefícios para o restante da vida. Recomendo a todos os leitores (no plural?) fazer um teste de 30 dias e ver como este exercício afeta sua produtividade. Se gostou, comente aqui embaixo. Se não gostou pode comentar também, mas seja educado ;)

Até mais.

sexta-feira, 2 de junho de 2017

Usando o profilehooks para medir tempo de execução em Python

Melhorias de performance em código Python já foram assunto de diversos textos neste blog. Já escrevi sobre utilização de [Cython] e [Numba], fiz uma comparação dessas duas tecnologias com Numpy e recentemente publiquei um texto sobre medição de consumo de memória. Neste texto irei tratar novamente de otimização de programas em Python, focando novamente em um exemplo de TRIOSlib (a biblioteca que desenvolvi durante meu doutorado). Uma das operações feitas é a contagem de pontos diferentes de zero em imagens. Já examinamos as diferenças entre Python puro, Cython e Numba neste operação anteriormente e chegamos à conclusão que o desempenho era melhor utilizando Numba e Cython, mas que mesmo os códigos usando Numpy já ofereciam grandes ganhos de desempenho.

Iremos otimizar a função num_samples(win) da classe Imageset (trios.imageset.Imageset). O código da função está abaixo. O código completo do arquivo pode ser visto aqui.

import os.path
import numpy as np
import trios.shortcuts.persistence as p
import collections

class Imageset:

    # .....

    def num_samples(self, win):
        total = 0
        for (_, _, m) in p.load_imageset(self, win):
            total += len(np.nonzero(m)[0])
        return total

    # .....

Na TRIOS um Imageset é uma lista de triplas contendo caminhos para imagens. A função num_samples(win) conta quantos pontos brancos existem na terceira imagem da tripla. A função load_imageset() carrega do disco as imagens gravadas em uma tripla e as retorna como matrizes do Numpy. Atualmente estamos usando Numpy para fazer a contagem de pontos.

Diferentemente dos textos anteriores, a função que iremos otimizar neste texto não é somente a implementação de um algoritmo em memória. Imageset.num_samples contém também acessos ao disco, o que significa que uma parte do tempo de execução não depende da implementação da contagem de pontos. Um dos nossos objetivos é verificar se a diferença obtida pela otimização na contagem de pontos é significativa dado o tempo de leitura das imagens do disco.


O módulo cProfile já vem incluso em Python e pode ser usado para medir quanto tempo é gasto por cada função de um programa. Sua utilização não é complicada, mas se torna ainda mais fácil com o módulo profilehooks, que é um wrapper para o cProfile usando o decorador @profile. Basta decorar uma função e um relatório é automaticamente impresso na saída padrão. Também é possível passar várias opções para o decorador.

Nosso código de testes é o seguinte:

from trios import imageset
from profilehooks import profile
import numpy as np

if __name__ == '__main__':
    imgset =
imageset.Imageset.read('/media/igor/Data1/datasets/staffs/level1.set')
    profiled_num_samples = profile(imgset.num_samples, entries=10)
    win = np.ones((11, 11), np.uint8)

    print('num_samples:', profiled_num_samples(win))
num_samples: 5595040

*** PROFILER RESULTS ***
num_samples (/home/igor/anaconda3/lib/python3.5/site-packages/trios-2.0.7-py3.5-linux-x86_64.egg/trios/imageset.py:65)
function called 1 times

         51097 function calls (50830 primitive calls) in 3.032 seconds

   Ordered by: cumulative time, internal time, call count
   List reduced from 322 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.003    0.003    3.032    3.032 imageset.py:65(num_samples)
       11    0.000    0.000    2.181    0.198 persistence.py:39(load_imageset)
       30    0.000    0.000    2.179    0.073 persistence.py:20(load_image)
       30    0.000    0.000    2.178    0.073 io.py:22(imread)
       30    0.000    0.000    2.178    0.073 pilutil.py:103(imread)
       30    0.000    0.000    2.148    0.072 pilutil.py:203(fromimage)
       30    0.057    0.002    2.148    0.072 {built-in method numpy.core.multiarray.array}
       30    0.001    0.000    2.090    0.070 Image.py:618(__array_interface__)
       30    0.005    0.000    2.089    0.070 Image.py:654(tobytes)
       30    0.008    0.000    1.968    0.066 ImageFile.py:120(load)

A execução da função num_samples com um conjunto de imagens contendo 10 imagens de alta resolução demorou 3 segundos, sendo que 2.2 segundos foram gastos na função load_imageset. Logo, por mais que otimizemos a contagem de pontos brancos, a execução demorará ao menos o tempo de ler as imagens do disco. Ou seja, mesmo para imagens em alta resolução uma pequena parte (cerca de 25%) do tempo é gasta contando os pontos brancos. Se possível, otimizar a leitura das imagens traria ganhos mais significativos.

A coluna ncalls mostra o número de vezes que a função daquela linha foi chamada, enquanto cumtime mostra o tempo cumulativo de execução (ou seja, o tempo da função executar por completo). Apesar de estarmos carregando somente 10 imagens p.load_image foi chamada 30 vezes! Examinando o código da função p.load_imageset vemos que ela retorna 3 imagens, mas só usamos a máscara. Logo, se lermos somente a máscara do disco poderemos economizar bastante tempo.

O novo código da função num_samples é

def num_samples(self, win):
    total = 0
    for (i, o, m) in self:
        if m is not None:
            m = p.load_mask_image(m, None, win)
            total += len(np.nonzero(m)[0])
        else:
            i = p.load_image(i)
            ww = win.shape[1]
            wh = win.shape[0]
            total += (i.shape[0] - wh+1)*(i.shape[1] - ww+1)
    return total
num_samples: 5595040

*** PROFILER RESULTS ***
num_samples2 (/home/igor/Dropbox/blog/otimizacao-trios/imageset.py:71)
function called 1 times

         22088 function calls (21821 primitive calls) in 1.618 seconds

   Ordered by: cumulative time, internal time, call count
   List reduced from 321 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.003    0.003    1.618    1.618 imageset.py:71(num_samples2)
       10    0.000    0.000    0.857    0.086 fromnumeric.py:1490(nonzero)
       10    0.856    0.086    0.856    0.086 {method 'nonzero' of 'numpy.ndarray' objects}
       10    0.002    0.000    0.758    0.076 persistence.py:23(load_mask_image)
       10    0.000    0.000    0.756    0.076 persistence.py:20(load_image)
       10    0.000    0.000    0.756    0.076 io.py:22(imread)
       10    0.000    0.000    0.756    0.076 pilutil.py:103(imread)
       10    0.000    0.000    0.732    0.073 pilutil.py:203(fromimage)
       10    0.016    0.002    0.732    0.073 {built-in method numpy.core.multiarray.array}
       10    0.001    0.000    0.716    0.072 Image.py:618(__array_interface__)

O consumo de tempo foi reduzido pela metade! Isto mostra a grande importância de medir qual função/parte de um script consome mais tempo. Podemos otimizar somente as partes que gastam a maior parte do tempo e identificar quais otimizações valem a pena. Além disto, agora a operação mais cara realmente é a contagem de pontos (linha 2, função nonzero em numeric.py). Mudando a contagem de pontos para a função np.count_nonzero obtemos a seguinte medição de tempo.

num_samples: 5595040

*** PROFILER RESULTS ***
num_samples3 (/home/igor/Dropbox/blog/otimizacao-trios/imageset.py:84)
function called 1 times

         22068 function calls (21801 primitive calls) in 1.073 seconds

   Ordered by: cumulative time, internal time, call count
   List reduced from 320 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.004    0.004    1.073    1.073 imageset.py:84(num_samples3)
       10    0.002    0.000    0.766    0.077 persistence.py:23(load_mask_image)
       10    0.000    0.000    0.765    0.076 persistence.py:20(load_image)
       10    0.000    0.000    0.765    0.076 io.py:22(imread)
       10    0.000    0.000    0.764    0.076 pilutil.py:103(imread)
       10    0.000    0.000    0.743    0.074 pilutil.py:203(fromimage)
       10    0.019    0.002    0.743    0.074 {built-in method numpy.core.multiarray.array}
       10    0.001    0.000    0.723    0.072 Image.py:618(__array_interface__)
       10    0.002    0.000    0.723    0.072 Image.py:654(tobytes)
       10    0.003    0.000    0.662    0.066 ImageFile.py:120(load)

Diminuimos a execução de 3 segundos no primeiro exemplo para somente 1 segundo. Executar as funções com @profile nos ajudou a encontrar onde a função gastava a maior quantidade de tempo e fizemos pequenas mudanças adicionais para ganhar ainda mais performance. É possível ir ainda mais longe, como pudemos ver no comparativo Numpy x Numba x Cython, porém o ganho de desempenho será pequeno para nosso caso de uso. É mais vantajoso manter o código mais enxuto e simples do que adicionar complexidade e dependências por um ganho pequeno. O código final da função é mostrado abaixo.

def num_samples3(self, win):
    total = 0
    for (i, o, m) in self:
        if m is not None:
            m = p.load_mask_image(m, None, win)
            total += np.count_nonzero(m)
        else:
            i = p.load_image(i)
            ww = win.shape[1]
            wh = win.shape[0]
            total += (i.shape[0] - wh+1)*(i.shape[1] - ww+1)
    return total

Espero que este texto tenha mostrado a importância da utilização de ferramentes de profiling para medição de tempo de execução em Python. Muitos scripts podem ser otimizados de maneira inteligente se nos concentrarmos somente nas partes que demoram a maior parte do tempo.