thegoodfellas/blogset-br huggingface dataset:
do 'Canvas' e a configuracao dos poligonos, nos retornando dois vetores. Esses vetores representam as variaveis de instancia minVals e maxVals de um cromossomo, sendo utilizados uma unica vez ao se instanciar a classe HillClimb:import java.awt.Color;import java.awt.Polygon;public class PolygonDecoder{ protected static PolygonColor[] chromosomeToPolygons(int[] data, int verts, int n, int colors) { int index = 0; int genes = 2 * verts + colors; int dim = genes * n; PolygonColor[] polygons = new PolygonColor[n]; int[] x = new int[verts]; int[] y = new int[verts]; int[] col = new int[colors]; for (int i = 0; i < dim; i += genes ) { //vertices int start = i; int end = start + verts; //x vertices int k = 0; for (int m = start; m < end; m++) x[k++] = data[m]; start = end; end = start + verts; //y vertices k = 0; for (int m = start; m < end; m++) y[k++] = data[m]; k = 0; //color for (int c = i + 2 * verts; c < i + 2 * verts + colors; c++) { col[k++] = data[c]; } PolygonColor pc = new PolygonColor(); pc.polygon = new Polygon(x, y, verts); pc.color = new Color(col[0], col[1], col[2], col[3]); polygons[index++] = pc; } return polygons; } protected static int[][] getChromosomeBounds(int w, int h, int verts,int n, int colors,int minAlpha, int maxAlpha) { int genes = 2 * verts + colors; int dim = genes * n; int[] minV = new int[dim]; int[] maxV = new int[dim]; for (int i = 0; i < dim; i += genes ) { int start = i; int end = start + verts; //x vertices for (int m = start; m < end; m++) maxV[m] = w; start = end; end = start + verts; //y vertices for (int m = start; m < end; m++) maxV[m] = h; //color for (int c = i + 2 * verts; c < i + 2 * verts + colors; c++) { //alpha component if (c == i + 2 * verts + colors - 1) { minV[c] = minAlpha; maxV[c] = maxAlpha; } //rgb component else maxV[c] = 255; } } return new int[][]{minV, maxV}; } } //fim da classeClasse EvolveDraw Essa e a classe que executa o nosso programa, tentando aproximar a imagem especificada em dentro do arquivo params.ini. Para obter bons resultados e fundamental usar parametros sensiveis, que funcionem bem em conjunto: vertices - Valores entre 3 e 6 parecem funcionar bem. n_poly - Dependendo da complexidade da imagem, valores entre 50 e 300 funcionam bem, porem o consumo de cpu aumenta bastante ao se utilizar um numero grande de poligonos. mutation_rate - Esse e talvez o parametro mais sensivel. Valores muito baixos ou muito altos nao funcionam bem, uma valor razoavel e 1.0/((vertices + 4) * n_poly). photo_interval_sec - O intervalo em segundos em que uma nova imagem da aproximacao e salva. Caso nao queira salvar as imagens, especifique um valor alto, ou comente a chamada ao metodo checkSnap e recompile. target_file - A imagem alvo, em formato .jpg. randomize_start -
= evolution.getBest(); polygons = PolygonDecoder.chromosomeToPolygons(best.fenotype, VERTICES, N_POLY, COLOR_COMP); repaint(); gen++; checkSnap(); } while (true); } public void paintComponent(Graphics g) { super.paintComponent(g); if (best == null) return; ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); for (PolygonColor pc : polygons) { g.setColor(pc.color); g.fillPolygon(pc.polygon); } } public BufferedImage createImage(JPanel panel) { int wid = panel.getWidth(); int heig = panel.getHeight(); BufferedImage bi = new BufferedImage(wid, heig, BufferedImage.TYPE_INT_ARGB); Graphics2D g = bi.createGraphics(); g.setBackground(bgColor); g.clearRect(0, 0, wid, heig); panel.paint(g); g.dispose(); return bi; } protected void checkSnap() { long time = System.currentTimeMillis(); long interval = time - lastPhotoTime; if (interval >= PHOTO_INTERVAL_SEC * 1000L) { this.lastPhotoTime = time; ImageUtils.saveJPG(createImage(this), TARGET_FILE + gen + ".jpg"); } } public float getFitness(int[] fenotype) { Graphics2D g = this.offImage.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setBackground(bgColor); g.clearRect(0, 0, w, h); PolygonColor[] polygons = PolygonDecoder.chromosomeToPolygons(fenotype, VERTICES, N_POLY, COLOR_COMP); for (PolygonColor pc : polygons) { g.setColor(pc.color); g.fillPolygon(pc.polygon); } g.dispose(); this.offImage.getRGB(0, 0, w, h, offImagePixels, 0, w); float diff = 0.0f; for (int i = 0; i < offImagePixels.length; i++) { int pixelOff = offImagePixels[i]; int alphaOff = (pixelOff >> 24) & 0xff; int redOff = (pixelOff >> 16) & 0xff; int greenOff = (pixelOff >> 8) & 0xff; int blueOff = (pixelOff) & 0xff; int alp = alphaTarget[i] - alphaOff; int red = redTarget[i] - redOff; int blue = blueTarget[i] - blueOff; int green = greenTarget[i] - greenOff; diff += alp * alp + red * red + blue * blue + green * green; } return 1.0f/(diff + 1.0f); } //executa public static void main (String[] args) { JFrame frame = new JFrame("Evolve Draw"); final EvolveDraw panel = new EvolveDraw(); frame.getContentPane().add(panel); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); frame.setResizable(false); Thread runnable = new Thread(panel); runnable.start(); } } //fim da classeUsamos uma abordagem bem simples, mas que ilustra bem o poder de um algoritmo evolucionario.Uma ideia para tentar melhorar o programa seria usar um vetor de poligonos onde o numero de vertices nao e mantido fixo, isso provavelmente iria acelerar a convergencia. Outra ideia consiste em 'injetar' os poligonos pouco a pouco, de acordo com alguma regra que leve em conta a taxa de aproximacao. Era isso, ate o proximo post!
row * cellSize + cellSize/2, cellSize/8, cellSize/8); } } } public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); this.drawGUI(g); this.drawBoard(g); this.drawScore(g); this.drawMoves(g); } //mouse private void addMouseWatcher() { this.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { int col = e.getX()/cellSize; int row = e.getY()/cellSize; int index = row * (OthelloBoard.BOARD_SIZE + 2) + col; if ((row > 0 && row < 9) && (col > 0 && col < 9)) doInput(index); } }); } public static void main (String[] args) throws Exception { OthelloPanel panel = new OthelloPanel(60, HUMAN); JFrame window = new JFrame("Othello"); window.getContentPane().add(panel); window.setResizable(false); window.pack(); window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); window.setVisible(true); new Thread(panel).start(); } } Eu particularmente nao consegui vencer a engine, pois como disse sou pessimo jogador. Testando contra algumas applets o desempenho foi bem convincente, perdendo apenas para os programas que utilizam inumeros padroes pre-calculados e implementam uma heuristica mais refinada. Bons jogos e ate o proximo post!
centers aceita um inteiro especificando o numero de clusters que desejamos identificar, ou uma matrix numerica para inicializacao dos centroides. Por fim, especificamos o numero maximo de iteracoes. Existe um terceiro argumento, algorithm, que nos permite especificar o algoritmo a ser usado - ficamos com o default. O codigo acima retorna um objeto da classe 'kmeans'. Podemos obter o centroide de cada cluster: centroids = km$centers E tambem o vetor que indica o cluster em que objeto foi alocado: classif = km$cluster Os resultados parecem fazer sentido, com algumas excecoes. Observe que Namibia, Georgia, Paquistao e India estao no mesmo grupo dos paises mais pobres, quando deveriam estar no grupo dos paises emergentes. Visualizando Nesta secao vamos usar o pacote ggplot2 para tentar visualizar melhor os resultados obtidos. Caso ainda nao o tenha instalado, de uma olhada neste post. Observando os resultados acima, vemos que os clusters 1, 2 e 3 foram atribuidos aos paises subdesenvolvidos, desenvolvidos e emergentes, respectivamente. Para facilitar a visualizacao do grafico, vamos substituir esses numeros pelas respectivas categorias: # cria um vetor de strings categorias = c('underdeveloped', 'developed', 'emergent') # aloca um vetor numerico de tamanho igual ao numero de paises cluster = numeric(nrow(paises)) # atribui a categoria[i] de acordo com o cluster for (i in 1:3) cluster[km$cluster == i] = categorias[i] Agora podemos usar o ggplot2: library(ggplot2) plotmatrix(paises, aes(colour = cluster)) + theme_bw() + opts(axis.title.x = theme_blank()) + opts(axis.title.y = theme_blank()) Para um grafico mais simples: pairs(paises, bg = km$cluster, pch = 21, cex = 1.5)Repare no agrupamento dos paises, de acordo com o esperado. Parece que o k-means funcionou bem neste caso. Era isso, ate a proxima!
Ola pessoal, tudo certo? Ja faz um tempo que nao posto nada, mas a partir deste mes pretendo escrever com maior frequencia. No post de hoje vamos falar um pouco sobre regressao logistica, uma tecnica bastante popular e sutil do ponto de vista teorico. Em Janeiro de 1986 o mundo inteiro acompanhou o desastre no lancamento do onibus espacial Challenger, que causou a morte dos sete tripulantes. Posteriormente, uma falha dos aneis de vedacao (no tanque externo de combustivel solido) foi apontada como a causa do desastre. Como exemplo vamos usar os dados de Temperatura x Falha do anel O-ring, que estavam disponiveis antes da tragedia. Introducao A regressao logistica pode ser usada quando desejamos modelar uma variavel categorica em funcao de uma ou mais variaveis preditoras (numericas, categoricas ou ambas). Na maioria dos casos, temos uma variavel resposta binaria e estamos interessado em quantificar as 'chances' de obter uma resposta positiva. Repare que nao podemos simplesmente usar uma regressao linear pelos seguintes motivos: A linha da regressao linear define o valor medio da variavel resposta, podendo assumir valores entre -Inf e +Inf. No caso de uma variavel binomial, o valor esperado esta restrito entre [0, 1]. Em segundo lugar, a regressao linear assume que a variancia do termo de erro e constante e independe dos valores assumidos pelos preditores. Isso nao ocorre quando a variavel resposta e binomial. Por fim, os residuos nao podem ser normalmente distribuidos, ja que a variavel resposta assume apenas dois valores possiveis. Uma possivel solucao e pensar no conceito de odds (chances). Seja Y a variavel resposta, que pode assumir os valores 0 ou 1, e Xn o vetor representando as n variaveis regressoras. A ideia e transformar Y em uma variavel numerica, e em seguida aplicar uma regressao linear. Para isso, seja p a probabilidade de Y assumir o valor 1, podemos entao definir: odds (p) = p/(1 - p) Repare que odds (Y) esta limitada entre [0, +Inf), o que ainda nos impede de aplicar uma regressao linear. Podemos entao definir a funcao logit: logit (p) = log(odds(p)) = log (p/(1 - p)) Agora estamos no intervalo (-Inf, +Inf) e podemos assumir um modelo linear nos preditores: log (p/(1 - p)) = βo + β1 x1 + β2 x2 + ... + βn xn Calculando a funcao inversa de logit (p), obtemos a funcao logistica: p (t) = 1/(1 + e-t) ou p = 1/(1 + e-(βo + β1 x1 + β2 x2 + ... + βn xn)) A equacao acima nos diz a probabilidade de se obter Y = 1 dado Xn. Observe que o modelo e linear entre logit (p) e Xn, porem, a relacao entre p e Xn e nao-linear. Por exemplo, ao se aumentar o preditor x1 em uma unidade (mantendo os demais constantes), odds (p) sofrera uma variacao dada por eβ1. Agora vamos ver como implementar os conceitos acima em R. R e GLM's Em R nao existe uma funcao especifica para ajustar um modelo de regressao logistica, e o motivo e simples: regressao logistica e apenas um caso
de modelo linear generalizado, ou GLM em ingles. Nesse tipo de modelo especificamos apenas a distribuicao do erro e a funcao de link. O codigo abaixo vai deixar tudo claro, entao baixe o dataset e salve no seu diretorio de trabalho. dados.nasa = read.csv('nasa_disaster.csv') modelo = glm(failure ~ temperature, data = dados.nasa, family = binomial(link = 'logit')) summary(modelo) Como nossa variavel de interesse e binaria (1 = houve falha, 0 = nao houve falha), especificamos uma distribuicao binomial e usamos a funcao logistica como link (que e o default nesse caso, veja help(glm)). Pelo output vemos que a temperatura possui efeito significativo na possibilidade de falhas (p value ~ 0.04). Observando os coeficientes do modelo, podemos escrever: log (p/(1 - p)) = 10.87535 - 0.17132 * T ou p = 1/(1 + e-(10.87535 - 0.17132 * T)) Onde p e a probabilidade de falha e T a temperatura. Graficamente: temp = dados.nasa$temperature p = modelo$fitted.values xlb = expression(paste('Temperatura [', degree,'F]')) titulo = 'NASA - Desastre Challenger\nProb falha x Temperatura' plot(temp, p, col = 'navy', pch = 4, cex = 1.5, ylab = 'p', xlab = xlb, main = titulo) grid(10, 10, col = '#CCCCCC') O grafico deixa claro a influencia da temperatura sobre a possibilidade de falha. Como comentado na introducao, ao variarmos a temperatura em 1 grau, as chances de falha variam em e-0.17132 = 0.8425515. Ou seja, ao aumentarmos a temperatura em 1 grau, as chances de falha sao reduzidas em 0.8425515. Esse valor e chamado de odds ratio, e e uma constante caracteristica do modelo. Podemos facilmente prever as probabilidades de falha para as temperaturas que nao estao no dataset original: new.temp = c(54, 55, 58, 59, 60, 61, 62, 64, 65, 71, 74, 77) new.data = data.frame(temperature = new.temp) probs = predict(modelo, new.data, type = 'response') points(new.temp, probs, pch = 3, col ='red', cex = 1.5) legenda = c('modelo', 'previsto') pontos.tipo = c(4, 3) cores = c('navy', 'red') legend('topright', legenda, col = cores, pch = pontos.tipo) A temperatura dos aneis de vedacao no dia do lancamento era de 31°F, valor bem distante dos valores usados no nosso modelo (dados de testes anteriores da NASA). Podemos dizer que uma previsao a essa temperatura provavelmente nao seria precisa, mas observando o grafico, fica claro que um lancamento a 31°F provavelmente vai resultar em uma falha nos aneis de vedacao. E incrivel como uma analise simples poderia ter ao menos alertado os Engenheiros. Voltando ao codigo, a funcao predict e bem simples de ser usada: fornecemos o modelo, um data frame com as novas temperaturas e especificamos o tipo como 'response', o que permite obter as probabilidades diretamente, ao inves dos log-odds. Lembrando que essa e uma funcao generica, portanto seu uso varia de acordo com o tipo de modelo (veja help(predict.glm)). O estudo de GLM's e vasto, essa foi apenas uma introducao pratica! Para mais exemplos de uma olhada em help(glm), e fique ligado nos posts. Era isso, ate a proxima!
= delegate.getFitness(this.current.fenotype); best = current; } public void run() { evolve(); if (signal != null) signal.countDown(); } public void evolve() { BinaryIntChromosome copyIndiv = current.duplicate(); copyIndiv.mutate(random, mutationRate); copyIndiv.decode(); copyIndiv.fitness = delegate.getFitness(copyIndiv.fenotype); if (copyIndiv.fitness > current.fitness) { current = copyIndiv; best = copyIndiv; } } public BinaryIntChromosome getBest() { return best; }}Testando Para finalizar este post, vamos testar as duas classes escritas e maximizar a funcao y = (x5 * x4 * x3) / (x2 * x1 * x0), cujo dominio esta entre xmin = [1, 2, 3, 4, 5, 6] e xmax = [10, 20, 30, 40, 50, 60]. Como voce deve ter notado, a solucao e trivial: S = [1, 2, 3, 40, 50, 60], porem o computador nao sabe disso!public class TestHC{ public static void main (String[] args) { int[] min = new int[]{1, 2, 3, 4, 5, 6}; int[] max = new int[]{10, 20, 30, 40, 50, 60}; float mutationRate = 0.05f; int generations = 600; FitnessDelegate delegate = new FitnessDelegate() { public float getFitness(int[] fenotype) { return (1.0f * fenotype[5] * fenotype[4] * fenotype[3]) / (fenotype[2] * fenotype[1] * fenotype[0]); } }; HillClimb hc = new HillClimb(min, max, delegate, mutationRate, null, true); for (int i = 0; i < generations; i++) hc.evolve(); BinaryIntChromosome best = hc.getBest(); System.out.println("Best solution: "); for (int i = 0; i < best.fenotype.length; i++) System.out.println("x[" + i + "] : " + best.fenotype[i]); }} Na proximo post colocaremos os poligonos a prova, ate mais!
Ola pesssoal, tudo bem? Nesse post vamos aplicar o famoso Minimax com poda alfa-beta, um dos meus algoritmos preferidos. Como aplicacao, nao queria escolher algo tao simples como Tic-Tac-Toe, nem tao complexo como xadrez. Foi ai que eu descobri o jogo Othello, tambem conhecido como Reversi. Nosso objetivo e criar uma engine simples, mas que jogue de maneira convincente. O interessante no Othello e que o jogo ainda nao foi 'resolvido' computacionalmente, pelo menos no tabuleiro padrao 8 x 8. Alem disso e extremamente simples de aprender, e as estrategias basicas sao faceis de codificar. Caso nao conheca o jogo, visite o link acima e tente algumas applets, depois volte aqui!Minimax A ideia basica do Minimax e minimizar a perda maxima, ou caso voce seja otimista, maximizar o ganho minimo. Para isso, a partir de determinada posicao, geramos a arvore com todas as posicoes possiveis ate determinada profundidade. Em seguida, percorremos essa arvore de baixo para cima, escolhendo os lances que maximizam nossos ganhos e lembrando que o nosso oponente fara o mesmo. Como temos um jogo de soma-zero , sob nosso pontos de vista o oponente ira minimizar seus ganhos, dai vem o nome. Se voce ja jogou xadrez contra uma engine forte, e facil perceber esse mecanismo de 'ganho minimo' em acao. Ao fazer um lance fraco ou anti-posicional, vemos como a engine pouco a pouco nos forca a uma posicao inferior, mesmo quando nos defendemos corretamente (minimizamos nossa perda maxima). Ao fazer um lance realmente ruim, o nosso score cai abruptmente e o da engine aumenta a mesma quantia, e nao e incomum um mate em 15 ou mais lances ser anunciado: provavelmente voce foi pego pela poda alfa-beta!Poda alfa-beta Voltando ao exemplo do xadrez, como uma engine consegue determinar quase que instantaneamente uma situacao de mate em 10, por exemplo? A resposta e simples: otimizacoes. O alpha-beta pruning, ou poda alfa-beta, e uma das otimizacoes que podem ser aplicadas ao Minimax, com o objetivo de reduzir o tempo de busca. Outra otimizacao que funciona junto com a poda alfa-beta e a ordenacao de lances. No exemplo do mate em 10, sao essas duas tecnicas trabalhando junto que fazem a engine ser tao eficiente. Para entender como isso funciona, imagine o seguinte: e a sua vez de mover e voce tem 20 lances disponiveis. Ao analizar a arvore do primeiro lance (A), percebemos que ele nos da uma vantagem decisiva em todas as variantes. Ao analizar o segundo lance disponivel (B), percebemos que este pode ser imediatamente neutralizado pelo adversario. Obviamente nao vale a pena continuar expandindo a arvore do lance B, pois sabemos que podemos fazer melhor com A. Ou seja, os nos provenientes de B nao serao expandidos, e assim ganhamos tempo de execucao. A ordenacao de lances acelera esse processo, colocando os lances mais promissores para serem avalidados primeiro. Para codificar esse mecanismo o algoritmo mantem duas variaveis, alpha e beta, que representam os melhores scores atingidos pelos jogadores ate entao.Heuristica Como vimos acima, precisamos de um metodo para decidir se uma determinada posicao e vantajosa
private ArrayList<Integer> flipSquares; private int idx; private int player; private int eval; public ArrayList<Integer> getFlipSquares() { return flipSquares; } public void setEval(int eval) { this.eval = eval; } public int getEval() { return this.eval; } public int getPlayer() { return this.player; } public void setPlayer(int player) { this.player = player; } public int getIdx() { return idx; } public void setFlipSquares(ArrayList<Integer> flipSquares) { this.flipSquares = flipSquares; } public void setIdx(int idx) { this.idx = idx; }}/* classe auxiliar */class MoveComparator implements Comparator<OthelloMove>{ public int compare(OthelloMove move1, OthelloMove move2) { if (move1.getEval() > move2.getEval()) return 1; else if (move1.getEval() < move2.getEval()) return -1; else return 0; }} Na segunda parte desta serie vamos implementar as classes restantes e vertestar a engine. Ate mais!
do diamante, tornando-o mais valioso. Color: A cor mais comum do diamante e o amarelo, e e uma das caracteristicas mais dificeis de se avaliar. Varia de Z (levemente amarelo) a D (completamente transparente e extremamente valioso). Clarity: representa a pureza interna do diamante. Obviamente, quanto menor o numero de impurezas, maior o valor do diamante. depth, table, x, y e z: sao as dimensoes do diamante, proporcionais ao volume. As variaveis acima sao conhecidas como os '4 C's do diamante'. Apos essa humilde introducao ao mundo dos diamante, vamos ao que interessa. Quick plot Como comentado anteriormente, ggplot2 usa uma especie de 'linguagem dos graficos', o que nos permite trabalhar em camadas e obter uma customizacao total do grafico. Por outro lado, ggplot2 nos oferece a funcao qplot, ou quick plot, que serve como 'atalho' para a criacao de graficos tradicionais. E exatamente essa funcao que vamos usar nos exemplos deste post.Scatter plots Como primeiro exemplo, vamos investigar a relacao entre o preco e o carat dos diamantes: qplot(carat, price, data = diamonds) ou qplot(x = carat, y = price, data = diamonds, geom = 'point') Ao investigar a assinatura da funcao (veja ?qplot), temos uma extensa lista de argumentos, vamos analisar os mais importantes: x: A coluna do data frame que sera plotada no eixo x. y: A coluna do data frame que sera plotada no eixo y. data: O data frame utilizado para extrair as variaveis especificadas. geom: Define o tipo de grafico. Repare que nesse caso nao precisamos especificar geom = 'point', pois ggplot2 usa 'point' como default quando fornecemos x e y. No caso de fornecermos somente x, ggplot2 usa 'histogram' como grafico default. Repare tambem que ao passar argumentos em R, podemos nomea-los ou nao. Quando seguimos a ordem definida na funcao, nao e necessaria nomear, mas quando nao sabemos ou nao temos certeza deste ordem, e sempre boa pratica passar os nomes. No primeiro caso acima, x e y sao os 2 primeiros argumentos, entao nao precisamos nomear (consulte o help com ?qplot). As tecnicas de manipulacao de dados usadas em R se aplicam normalmente. Por exemplo, podemos observar a relacao entre o carat e o volume dos diamantes: qplot(carat, x * y * z, data = diamonds, ylab = 'volume') Alem do basico Os exemplos anteriores sao bastante simples, vamos tentar algo que mostre as vantagens do ggplot2. O dataset diamonds e muito grande, vamos usar uma amostra aleatoria deste dataset: nlinhas = 500 diamantes.amostra = diamonds[sample(nrow(diamonds), nlinhas), ] Suponha agora que precisamos plotar preco x carat, mas queremos que diamantes com cores diferentes sejam plotados com cores diferentes. Usando ggplot2, basta fazer qplot(carat, price, data = diamantes.amostra, colour = color) e obtemos o grafico acima. Repare no argumento colour = color, ou seja, separamos a cor de cada ponto (atributo colour) de acordo com a coluna 'color' do dataset. Da mesma maneira, podemos plotar cada ponto com um simbolo diferente, de acordo com o corte do diamante (coluna 'cut'): qplot(carat, price, data = diamantes.amostra, shape = cut) Nesse caso especificamos shape = cut,