
Esta é uma série de artigos sobre minha jornada em andamento na floresta escura das competições do
Kaggle como desenvolvedor .NET.
Estarei focando em (quase) redes neurais puras neste e nos seguintes artigos. Isso significa que a maioria das partes chatas da preparação do conjunto de dados, como preencher valores ausentes, seleção de recursos, análise de outliers, etc. será ignorado intencionalmente.
A pilha de tecnologia será a API C # + TensorFlow
tf.keras . A partir de hoje, também será necessário o Windows. Modelos maiores nos próximos artigos podem precisar de uma GPU adequada para que seu tempo de treinamento permaneça saudável.
Vamos prever os preços dos imóveis!
Os preços da habitação são uma grande competição para iniciantes. Seu conjunto de dados é pequeno, não há regras especiais, o placar público tem muitos participantes e você pode enviar até 4 entradas por dia.
Registre-se no Kaggle, se você ainda não o fez, participe desta competição e faça o download dos dados. O objetivo é prever o preço de venda (coluna Preço de venda) para entradas em
test.csv . O arquivo contém
train.csv , que possui cerca de 1500 entradas com preço de venda conhecido para treinamento. Começaremos com o carregamento desse conjunto de dados e a exploração um pouco antes de entrar nas redes neurais.
Analisar dados de treinamento
Eu disse que pularemos a preparação do conjunto de dados? Eu menti! Você tem que dar uma olhada pelo menos uma vez.Para minha surpresa, não encontrei uma maneira fácil de carregar um arquivo .csv na biblioteca de classes padrão do .NET. Instalei um pacote NuGet, chamado
CsvHelper . Para simplificar a manipulação de dados, também recebi meu novo pacote de extensão LINQ favorito
MoreLinq .
Carregando dados .csv no DataTablestatic DataTable LoadData(string csvFilePath) { var result = new DataTable(); using (var reader = new CsvDataReader(new CsvReader(new StreamReader(csvFilePath)))) { result.Load(reader); } return result; }
ML.NETUsar o
DataTable para treinar a manipulação de dados é, na verdade, uma péssima idéia.
O ML.NET deve ter o carregamento .csv e muitas operações de comparação e exploração de dados. No entanto, ainda não estava pronto para esse fim específico, quando entrei na competição de Preços da habitação.
Os dados são assim (apenas algumas linhas e colunas):
Id | MSSubClass | Msoning | LotFrontage | Lotarea |
1 | 60 | RL | 65 | 8450 |
2 | 20 | RL | 80 | 9600 |
3 | 60 | RL | 68 | 11250 |
4 | 70 | RL | 60 | 9550 |
Depois de carregar os dados, precisamos remover a coluna
Id , pois ela não tem relação com os preços da habitação:
var trainData = LoadData("train.csv"); trainData.Columns.Remove("Id");
Analisando os tipos de dados da coluna
O DataTable não infere automaticamente os tipos de dados das colunas e assume que são todas as strings. Portanto, o próximo passo é determinar o que realmente temos. Para cada coluna, calculei as seguintes estatísticas: número de valores distintos, quantos deles são números inteiros e quantos são números de ponto flutuante (um código-fonte com todos os métodos auxiliares será vinculado no final do artigo):
var values = rows.Select(row => (string)row[column]); double floats = values.Percentage(v => double.TryParse(v, out _)); double ints = values.Percentage(v => int.TryParse(v, out _)); int distincts = values.Distinct().Count();
Colunas numéricas
Acontece que a maioria das colunas é realmente ints, mas como as redes neurais funcionam principalmente em números flutuantes, nós as converteremos em dobras de qualquer maneira.
Colunas categóricas
Outras colunas descrevem as categorias às quais a propriedade à venda pertencia. Nenhum deles tem muitos valores diferentes, o que é bom. Para usá-los como entrada para nossa futura rede neural, eles também precisam ser convertidos em
duplicados .
Inicialmente, simplesmente atribuai números de 0 a distintoValueCount - 1 a eles, mas isso não faz muito sentido, pois na verdade não há progressão de "Fachada: Azul" por "Fachada: Verde" para "Fachada: Branco". Tão cedo mudei isso para o que é chamado
de codificação one-hot , em que cada valor exclusivo recebe uma coluna de entrada separada. Por exemplo, “Fachada: Azul” se torna [1,0,0] e “Fachada: Branco” se torna [0,0,1].
Reunindo todos eles
Grande produção de exploração de dadosCentralAir: 2 valores, ints: 0,00%, flutuadores: 0,00%
Rua: 2 valores, ints: 0,00%, carros alegóricos: 0,00%
Utilitários: 2 valores, ints: 0,00%, flutuadores: 0,00%
Beco: 3 valores, ints: 0,00%, carros alegóricos: 0,00%
BsmtHalfBath: 3 valores, ints: 100,00%, flutuadores: 100,00%
HalfBath: 3 valores, ints: 100,00%, flutuadores: 100,00%
LandSlope: 3 valores, ints: 0,00%, flutuadores: 0,00%
Unidade Pavimentada: 3 valores, ints: 0,00%, flutuadores: 0,00%
BsmtFullBath: 4 valores, ints: 100.00%, flutuadores: 100.00%
ExterQual: 4 valores, ints: 0,00%, flutuadores: 0,00%
Lareiras: 4 valores, ints: 100,00%, flutuadores: 100,00%
FullBath: 4 valores, ints: 100,00%, flutuadores: 100,00%
GarageFinish: 4 valores, ints: 0.00%, flutuadores: 0.00%
KitchenAbvGr: 4 valores, ints: 100,00%, flutuadores: 100,00%
CozinhaQual: 4 valores, ints: 0.00%, flutuadores: 0.00%
LandContour: 4 valores, ints: 0,00%, flutuadores: 0,00%
LotShape: 4 valores, ints: 0,00%, flutuadores: 0,00%
PoolQC: 4 valores, ints: 0,00%, flutuadores: 0,00%
BldgType: 5 valores, ints: 0.00%, flutuadores: 0.00%
BsmtCond: 5 valores, ints: 0,00%, flutuadores: 0,00%
Exposição: 5 valores, ints: 0,00%, flutuadores: 0,00%
BsmtQual: 5 valores, ints: 0,00%, flutuadores: 0,00%
ExterCond: 5 valores, ints: 0,00%, flutuadores: 0,00%
Cerca: 5 valores, ints: 0,00%, flutuadores: 0,00%
GarageCars: 5 valores, ints: 100,00%, carros alegóricos: 100,00%
AquecimentoQC: 5 valores, ints: 0,00%, flutuadores: 0,00%
LotConfig: 5 valores, ints: 0,00%, flutuações: 0,00%
MasVnrType: 5 valores, ints: 0,00%, flutuadores: 0,00%
Característica: 5 valores, ints: 0,00%, flutuadores: 0,00%
MSZoning: 5 valores, ints: 0.00%, floats: 0.00%
Vendido: 5 valores, ints: 100,00%, flutuadores: 100,00%
Elétrica: 6 valores, ints: 0,00%, flutuadores: 0,00%
LareiraQu: 6 valores, ints: 0.00%, flutuadores: 0.00%
Fundação: 6 valores, ints: 0,00%, flutuadores: 0,00%
GarageCond: 6 valores, ints: 0,00%, flutuadores: 0,00%
GarageQual: 6 valores, ints: 0,00%, flutuadores: 0,00%
Aquecimento: 6 valores, ints: 0,00%, flutuadores: 0,00%
RoofStyle: 6 valores, ints: 0,00%, flutuadores: 0,00%
Condição: 6 valores, ints: 0,00%, flutuadores: 0,00%
BsmtFinType1: 7 valores, ints: 0.00%, flutuações: 0.00%
BsmtFinType2: 7 valores, ints: 0,00%, flutuações: 0,00%
Funcional: 7 valores, ints: 0,00%, flutuadores: 0,00%
GarageType: 7 valores, ints: 0,00%, flutuadores: 0,00%
BedroomAbvGr: 8 valores, ints: 100.00%, flutuadores: 100.00%
Condição2: 8 valores, ints: 0,00%, flutuadores: 0,00%
HouseStyle: 8 valores, ints: 0.00%, flutuadores: 0.00%
PoolArea: 8 valores, ints: 100,00%, flutuadores: 100,00%
RoofMatl: 8 valores, ints: 0,00%, flutuadores: 0,00%
Condição1: 9 valores, ints: 0,00%, flutuadores: 0,00%
GeralCond: 9 valores, ints: 100,00%, flutuadores: 100,00%
Tipo de venda: 9 valores, ints: 0,00%, flutuadores: 0,00%
TotalQual: 10 valores, ints: 100,00%, flutuadores: 100,00%
MoSold: 12 valores, ints: 100,00%, flutuadores: 100,00%
TotRmsAbvGrd: 12 valores, ints: 100,00%, flutuações: 100,00%
Exterior1st: 15 valores, ints: 0,00%, flutuadores: 0,00%
MSSubClass: 15 valores, ints: 100,00%, flutuadores: 100,00%
Exterior2º: 16 valores, ints: 0,00%, flutuadores: 0,00%
3SsnPorch: 20 valores, ints: 100,00%, flutuadores: 100,00%
MiscVal: 21 valores, ints: 100,00%, flutuadores: 100,00%
LowQualFinSF: 24 valores, ints: 100,00%, flutuadores: 100,00%
Bairro: 25 valores, ints: 0,00%, flutuadores: 0,00%
YearRemodAdd: 61 valores, ints: 100.00%, flutuadores: 100.00%
ScreenPorch: 76 valores, ints: 100,00%, flutuadores: 100,00%
GarageYrBlt: 98 valores, ints: 94.45%, flutuadores: 94.45%
LotFrontage: 111 valores, ints: 82.26%, flutuadores: 82.26%
Ano de construção: 112 valores, polegadas: 100,00%, flutuadores: 100,00%
Porch fechado: 120 valores, ints: 100,00%, flutuadores: 100,00%
BsmtFinSF2: 144 valores, ints: 100,00%, flutuadores: 100,00%
OpenPorchSF: 202 valores, ints: 100,00%, flutuadores: 100,00%
WoodDeckSF: 274 valores, ints: 100,00%, flutuadores: 100,00%
MasVnrArea: 328 valores, ints: 99.45%, flutuações: 99.45%
2ndFlrSF: 417 valores, ints: 100.00%, flutuadores: 100.00%
GarageArea: 441 valores, ints: 100,00%, flutuadores: 100,00%
BsmtFinSF1: 637 valores, ints: 100.00%, flutuações: 100.00%
Preço: 663 valores, ints: 100,00%, flutuadores: 100,00%
TotalBsmtSF: 721 valores, ints: 100,00%, flutuações: 100,00%
1stFlrSF: 753 valores, ints: 100.00%, flutuadores: 100.00%
BsmtUnfSF: 780 valores, ints: 100,00%, flutuadores: 100,00%
GrLivArea: 861 valores, ints: 100,00%, flutuadores: 100,00%
LotArea: 1073 valores, ints: 100.00%, flutuações: 100.00%
Muitas colunas de valor:
Exterior1st: AsbShng, AsphShn, BrkComm, BrkFace, CBlock, CemntBd, HdBoard, ImStucc, MetalSd, Contraplacado, Pedra, Estuque, VinylSd, Wd Sdng, WdShing
Exterior2nd: AsbShng, AsphShn, Brk Cmn, BrkFace, CBlock, CmentBd, HdBoard, ImStucc, MetalSd, Outros, Contraplacado, Pedra, Estuque, VinylSd, Wd Sdng, Wd Shng
Bairro: Blmngtn, Blueste, BrDale, BrkSide, ClearCr, CollgCr, Crawfor, Edwards, Gilbert, IDOTRR, MeadowV, Mitchel, NAmes, NoRidge, NPkVill, NridgHt, NWAmes, OldTown, Sawyer, SawyerW, Somerst, Stone, Stoner Veenker
carros alegóricos não analisáveis
GarageYrBlt: NA
LotFrontage: NA
MasVnrArea: NA
gamas de flutuação:
BmmHalfBath: 0 ... 2
HalfBath: 0 ... 2
BmmFullBath: 0 ... 3
Lareiras: 0 ... 3
FullBath: 0 ... 3
KitchenAbvGr: 0 ... 3
Garagem: 0 ... 4
Ano: 2006 ... 2010
QuartoAbvGr: 0 ... 8
PoolArea: 0 ... 738
GeralCondição: 1 ... 9
TotalQualidade: 1 ... 10
MoSold: 1 ... 12
TotRmsAbvGrd: 2 ... 14
MSSubClass: 20 ... 190
3SsnPorch: 0 ... 508
Valor: 0 ... 15500
LowQualFinSF: 0 ... 572
Adicionado: 1950 ... 2010
Porteiro: 0 ... 480
GaragemYrBlt: 1900 ... 2010
Fronteira do lote: 21 ... 313
Ano de construção: 1872 ... 2010
Varanda fechada: 0 ... 552
BsmtFinSF2: 0 ... 1474
OpenPorchSF: 0 ... 547
WoodDeckSF: 0 ... 857
MasVnrArea: 0 ... 1600
2ndFlrSF: 0 ... 2065
Área de Garagem: 0 ... 1418
BsmtFinSF1: 0 ... 5644
Preço: 34.900 ... 755.000
TotalBsmtSF: 0 ... 6110
1stFlrSF: 334 ... 4692
BsmtUnfSF: 0 ... 2336
GrLivArea: 334 ... 5642
Área do lote: 1300 ... 215245
Com isso em mente,
criei o seguinte
ValueNormalizer , que obtém algumas informações sobre os valores dentro da coluna e retorna uma função que transforma um valor (uma
string ) em um vetor de recurso numérico para a rede neural (
double [] ):
ValueNormalizer static Func<string, double[]> ValueNormalizer( double floats, IEnumerable<string> values) { if (floats > 0.01) { double max = values.AsDouble().Max().Value; return s => new[] { double.TryParse(s, out double v) ? v / max : -1 }; } else { string[] domain = values.Distinct().OrderBy(v => v).ToArray(); return s => new double[domain.Length+1] .Set(Array.IndexOf(domain, s)+1, 1); } }
Agora, temos os dados convertidos em um formato adequado para uma rede neural. É hora de construir um.
Construa uma rede neural
A partir de hoje, você precisaria usar uma máquina Windows para isso.Se você já possui o Python e o TensorFlow 1.1x instalados, tudo o que você precisa é
<PackageReference Include="Gradient" Version="0.1.10-tech-preview3" />
no seu arquivo .csproj moderno. Caso contrário, consulte o
manual do
Gradiente para fazer a configuração inicial.
Quando o pacote estiver em funcionamento, podemos criar nossa primeira rede superficial rasa.
using tensorflow; using tensorflow.keras; using tensorflow.keras.layers; using tensorflow.train; ... var model = new Sequential(new Layer[] { new Dense(units: 16, activation: tf.nn.relu_fn), new Dropout(rate: 0.1), new Dense(units: 10, activation: tf.nn.relu_fn), new Dense(units: 1, activation: tf.nn.relu_fn), }); model.compile(optimizer: new AdamOptimizer(), loss: "mean_squared_error");
Isso criará uma rede neural não treinada com 3 camadas de neurônios e uma camada de evasão, que ajuda a evitar o super ajuste.
tf.nn.relu_fntf.nn.relu_fn é a função de ativação de nossos neurônios.
Sabe- se que
ReLU funciona bem em redes profundas, porque resolve o
problema de gradiente de fuga : derivadas de funções originais de ativação não linear tendem a se tornar muito pequenas quando o erro é propagado de volta da camada de saída em redes profundas. Isso significava que as camadas mais próximas da entrada se ajustariam apenas um pouco, o que atrasava significativamente o treinamento de redes profundas.
AbandonoA evasão é uma camada de função especial nas redes neurais, que na verdade não contém neurônios como tal. Em vez disso, ele opera tomando cada entrada individual e a substitui aleatoriamente por 0 na saída automática (caso contrário, apenas passa o valor original). Ao fazer isso, ajuda a evitar o
ajuste excessivo a recursos menos relevantes em um pequeno conjunto de dados. Por exemplo, se não removemos a coluna
Id , a rede poderia potencialmente memorizar exatamente o <Id> -> <SalePrice> mapeamento, o que nos daria 100% de precisão no conjunto de treinamento, mas números completamente independentes em qualquer outro dado.
Por que precisamos de abandono? Nossos dados de treinamento têm apenas ~ 1.500 exemplos e essa pequena rede neural que construímos tem> 1800 pesos ajustáveis. Se fosse um polinômio simples, poderia corresponder à função de preço que estamos tentando aproximar exatamente. Mas, então, ele teria valores enormes em quaisquer entradas fora do conjunto de treinamento original.
Alimente os dados
O TensorFlow espera seus dados em matrizes NumPy ou nos tensores existentes. Estou convertendo DataRows em matrizes NumPy:
using numpy; ... const string predict = "SalePrice"; ndarray GetInputs(IEnumerable<DataRow> rowSeq) { return np.array(rowSeq.Select(row => np.array( columnTypes .Where(c => c.column.ColumnName != predict) .SelectMany(column => column.normalizer( row.Table.Columns.Contains(column.column.ColumnName) ? (string)row[column.column.ColumnName] : "-1")) .ToArray())) .ToArray() ); } var predictColumn = columnTypes.Single(c => c.column.ColumnName == predict); ndarray trainOutputs = np.array(predictColumn.trainValues .AsDouble() .Select(v => v ?? -1) .ToArray()); ndarray trainInputs = GetInputs(trainRows);
No código acima, convertemos cada
DataRow em um
ndarray , pegando todas as células nele e aplicando o
ValueNormalizer correspondente à sua coluna. Em seguida, colocamos todas as linhas em outro
ndarray , obtendo uma matriz de matrizes.
Nenhuma transformação é necessária para saídas, onde apenas convertemos valores de trem em outro
ndarray .
Hora de descer o gradiente
Com essa configuração, tudo o que precisamos fazer para treinar nossa rede é chamar a função de
ajuste do modelo:
model.fit(trainInputs, trainOutputs, epochs: 2000, validation_split: 0.075, verbose: 2);
Na verdade, esta chamada anulará os últimos 7,5% do treinamento definido para validação e repita as seguintes 2000 vezes:
- dividir o restante do trainInputs em lotes
- alimentar esses lotes, um por um, na rede neural
- erro de cálculo usando a função de perda que definimos acima
- retropropagam o erro através dos gradientes de conexões individuais de neurônios, ajustando pesos
Durante o treinamento, ele emitirá o erro da rede nos dados reservados para validação como
val_loss e o erro nos dados de treinamento em si como apenas
perda . Geralmente, se
val_loss se torna muito maior do que a
perda , significa que a rede começou a se
ajustar demais. Abordarei isso com mais detalhes nos seguintes artigos.
Se você fez tudo corretamente, uma
raiz quadrada de uma de suas perdas deve ser da ordem de 20000.

Submissão
Não falarei muito sobre gerar o arquivo para enviar aqui. O código para calcular resultados é simples:
const string SubmissionInputFile = "test.csv"; DataTable submissionData = LoadData(SubmissionInputFile); var submissionRows = submissionData.Rows.Cast<DataRow>(); ndarray submissionInputs = GetInputs(submissionRows); ndarray sumissionOutputs = model.predict(submissionInputs);
que usa principalmente funções definidas anteriormente.
Em seguida, é necessário gravá-los em um arquivo .csv, que é simplesmente uma lista de pares Id, predicted_value.
Ao enviar seu resultado, você deve obter uma pontuação na ordem de 0,17, que estaria em algum lugar no último trimestre da tabela de classificação pública. Mas, ei, se fosse tão simples quanto uma rede de 3 camadas com 27 neurônios, esses irritantes cientistas de dados não receberiam compensações totais de US $ 300k + / a das principais empresas americanas
Finalizando
O código fonte completo dessa entrada (com todos os ajudantes e algumas das partes comentadas de minhas explorações e experiências anteriores) é de cerca de 200 linhas no
PasteBin .
No próximo artigo, você verá minhas travessuras tentando entrar no top 50% dessa classificação pública. Vai ser uma aventura amadora para viajantes, uma briga com o Windmill of Overfitting com a única ferramenta que o viajante tem - um modelo maior (por exemplo, NN profundo, lembre-se, não há engenharia manual de recursos!). Será menos um tutorial de codificação e mais uma busca de raciocínio com matemática realmente complicada e uma conclusão estranha. Fique atento!
Ligações
KaggleCompetição de preços da habitação no KaggleTutorial de regressão do TensorFlowPágina inicial do TensorFlowReferência da API do TensorFlowGradiente (ligação ao TensorFlow)