Вопрос или проблема
Я пытался написать классификатор наивного байеса с нуля, который должен предсказать класс метки набора данных nominal car.arff. Однако классификатор всегда предсказывает самый распространенный класс. Я попробовал логарифмические вероятности и коррекцию Лапласа, но это не дало результатов. Также я заметил, что условные вероятности для любого атрибута всегда наивысшие для самого распространенного класса. Это связано с моим набором данных? Что с этим можно сделать?
Вот мой код:
import numpy as np
import pandas as pd
from scipy.io import arff
def parser(path):
"""
функция, которая разбирает данные из файла arff
@param path: строка, содержащая путь к файлу
@return массив, содержащий данные
@raise FileNotFoundError исключение в случае, если путь не указывает на действительный файл
"""
start = 0 # проверка, действительно ли имеются данные
# Декларативы как константы, чтобы избежать опечаток в коде
RELATION = 'relation'
ATTRIBUTE = 'attribute'
DATA = 'data'
# Создать словарь, хранящий информацию arff
data = {RELATION: [],
ATTRIBUTE: [],
DATA: []}
# Чтение файла и анализ данных
with open(path) as file:
for line in file.readlines():
# Проверка, пустая ли строка
if line.strip() == '':
continue
# Проверка, содержит ли строка отношение
elif '@' + RELATION in line:
data[RELATION].append(line.replace('@' + RELATION, '').strip())
# Проверка, содержит ли строка атрибут
elif line.startswith('@attribute'):
tmp = line.replace("{", "").replace("}", "").replace("\n", "").replace("'", "")
# проверяет, есть ли пробелы между запятыми в атрибутах
if (len(tmp.split(" ")) > 3):
values = tmp.replace(",", "").split(" ")[2:]
else:
values = tmp.split(" ")[2].split(",")
data[ATTRIBUTE].append({'name': tmp.split(" ")[1], 'values': values})
# проверка, существует ли @data
elif '@' + DATA in line:
start = 1
# Если строка не является одной из других, она должна содержать данные
elif '@' + DATA not in line and start:
line = line.split(',')
# обрезать каждый элемент строки
for i in range(len(line)):
line[i] = line[i].strip()
# Добавить данные в словарь
data[DATA].append(line)
attributes = np.array(data['attribute'])
out = []
for i in range(len(data['data'])):
data_dict = {}
for j in range(len(attributes)):
data_dict.update({attributes[j]['name']: data['data'][i][j]})
out.append(data_dict)
out = np.array(out)
return out, data[ATTRIBUTE]
class NaiveBayes():
def __init__(self, data, atts, class_label):
self.data = data
self.atts = atts
self.class_label = class_label
def prior(self):
prior_probabilities = [0,0,0,0]
for i in range(len(self.data)):
if self.data[i]['class'] == 'unacc': prior_probabilities[0] += 1
if self.data[i]['class'] == 'acc': prior_probabilities[1] += 1
if self.data[i]['class'] == 'good': prior_probabilities[2] += 1
if self.data[i]['class'] == 'vgood': prior_probabilities[3] += 1
prior_probabilities = [x/len(self.data) for x in prior_probabilities]
return prior_probabilities
def conditionalProbability(self,key,value,length):
# возвращает (в нашем случае) 4-мерный вектор для одного атрибута с вероятностями для каждого исхода
conditional_probabilities = [0]*length
# определенно не самый эффективный способ
for i in range(len(self.data)):
if self.data[i][key] == value:
if self.data[i]['class'] == 'unacc': conditional_probabilities[0] += 1
if self.data[i]['class'] == 'acc': conditional_probabilities[1] += 1
if self.data[i]['class'] == 'good': conditional_probabilities[2] += 1
if self.data[i]['class'] == 'vgood': conditional_probabilities[3] += 1
s = np.sum(conditional_probabilities)
conditional_probabilities = [x/s for x in conditional_probabilities]
return conditional_probabilities
def classification(self, instance):
cprobs = []
probs = self.prior()
for key in instance.keys():
cprobs.append(self.conditionalProbability(key,instance[key],4))
print(cprobs)
# получение вероятностей
predicted_class = "unacc"
for i in range(len(cprobs)-1):
for j in range(4):
probs[j]*=cprobs[i][j]
# print(instance)
print(probs)
return probs.index(max(probs))
raw,atts = parser('car.arff')
class_attribute="class"
classifier = NaiveBayes(raw,atts,class_attribute)
print(classifier.data[1])
print(classifier.prior())
print(classifier.conditionalProbability('buying','vhigh',4))
print(classifier.classification(classifier.data[0]))
'''
results = [0,0,0,0]
for i in range(len(classifier.data)):
results[classifier.classification(classifier.data[i])]+=1
print(results)
'''
Это распределение классов и некоторая дополнительная информация:
% 5. Количество экземпляров: 1728
% (экземпляры полностью охватывают пространство атрибутов)
%
% 6. Количество атрибутов: 6
%
% 7. Значения атрибутов:
%
% buying v-high, high, med, low
% maint v-high, high, med, low
% doors 2, 3, 4, 5-more
% persons 2, 4, more
% lug_boot small, med, big
% safety low, med, high
%
% 8. Пропущенные значения атрибутов: нет
%
% 9. Распределение классов (количество экземпляров по классам)
%
% класс N N[%]
% -----------------------------
% unacc 1210 (70.023 %)
% acc 384 (22.222 %)
% good 69 ( 3.993 %)
% v-good 65 ( 3.762 %)
и вот несколько примеров данных:
low,low,5more,more,small,low,unacc
low,low,5more,more,small,med,acc
low,low,5more,more,small,high,good
low,low,5more,more,med,low,unacc
low,low,5more,more,med,med,good
low,low,5more,more,med,high,vgood
low,low,5more,more,big,low,unacc
low,low,5more,more,big,med,good
low,low,5more,more,big,high,vgood
Полный набор данных можно найти здесь
Глядя на ваше распределение классов, оно сильно несбалансировано, и это может искажать модель, предсказывая класс большинства, который в данном случае является 'unacc'
. Таким образом, одной из рекомендаций будет сбалансировать классы, как правило, добавляя больше экземпляров классов меньшинства, чтобы они были равны классу большинства.
Также, глядя на ваши образцы данных, кажется, что между buying, maint, doors и persons немного или вообще нет вариации, и здесь, похоже, эти признаки не будут влиять на решение о классификации.
В этом случае я бы вернулся к исследованию данных и посмотрел, какие признаки могут повлиять на решение о классификации. Это можно сделать с помощью столбчатых диаграмм и гистограмм. При этом разделите данные на классы и постройте распределение признаков по классам, чтобы увидеть, есть ли заметная вариация в распределении этих признаков по их классам.
Ответ или решение
Обучение классификатора на основе наивного байесовского подхода может иногда приводить к результатам, когда он постоянно прогнозирует один и тот же класс. В вашем случае, модель всегда предсказывает самый распространенный класс, что указывает на несколько возможных причин. Давайте рассмотрим основные факторы, влияющие на такое поведение, и предложим пути их решения.
1. Непропорциональное распределение классов
Как вы упомянули в описании, ваш набор данных имеет значительное неравномерное распределение классов. Действительно, класс 'unacc'
составляет более 70% всех экземпляров, в то время как остальные классы ('acc'
, 'good'
, 'vgood'
) имеют значительно меньшее представительство. Это может заставить модель придерживаться предсказания для большинства класса, поскольку она стремится минимизировать ошибку.
Решения:
- Балансировка классов: Используйте методы балансировки классов, например, увеличьте количество экземпляров меньшинств за счет синтетической генерации данных (например, метод SMOTE) или выберите случайные экземпляры из большего класса, чтобы создать более сбалансированное представительство.
- Изменение весов классов: Учитывайте веса классов в функции потерь, чтобы уменьшить влияние классов с высоким представлением.
2. Слабая информативность признаков
Ваша модель может не находить полезных взаимосвязей между признаками и классами. Если большинство признаков не могут применять различия между классами, это может привести к ситуациям, когда модель просто предсказывает самый распространенный класс.
Решения:
- Анализ признаков: Исследуйте взаимосвязи между признаками и классами. Постройте графики и гистограммы, чтобы визуализировать распределение признаков по классам. Это поможет выявить, какие признаки имеют значительное влияние на классификацию.
- Выбор признаков: Удалите или объедините менее значимые признаки, чтобы улучшить качество модели. Выбор признаков может уменьшить шум и повысить производительность.
3. Неправильная реализация вероятностных функций
Также есть вероятность, что реализация ваших методов может содержать ошибки, которые мешают корректному расчёту вероятностей.
Решения:
- Лапласова коррекция: Убедитесь, что вы используете Лапласову коррекцию в функции условной вероятности, чтобы избежать нулевых вероятностей для редко встречающихся классов.
- Проверка кодирования: Перепроверьте вашу реализацию расчетов вероятностей и убедитесь, что они соответствуют формулям, используемым в наивном байесовском классификаторе.
Заключение
Проблема предсказания только одного класса в вашем наивном байесовском классификаторе может быть связана как с неравномерным распределением классов в наборе данных, так и с потенциалом слабой информативности признаков или ошибками в коде. Рекомендуется начать с анализа распределения классов, затем провести исследование признаков и их влияния, а также тщательно проверить вашу реализацию вероятностных функций. Такие действия должны помочь улучшить общую точность вашей модели.