Когда мы просматриваем новости, мы обычно видим новостные сайты, классифицирующие каждую новость:
Область применения новостной классификации весьма обширна. Для веб-сайтов он может рекомендовать вам новости в соответствии с категориями новостей, которые вы чаще всего видите; для пользователей можно игнорировать категории, которые вам не интересны, что улучшает работу в Интернете.
Например, я просканировал 137 000 новостей, рекомендованных мне приложением NetEase News APP, в течение почти месяца Следующая карта категорий новостей полностью показывает, что я непрофессионал, который любит смотреть спортивные и развлекательные новости:
Так как же классифицируют новости новостные классификации крупных новостных веб-сайтов? Понятно, что редакторы веб-сайтов могут классифицировать вручную, но в настоящее время более вероятно, что они будут автоматически классифицированы с помощью различных продвинутых алгоритмов и ИИ.
В этой статье с использованием Python и Keras показано, как начать сбор данных, проанализировать их, предварительно обработать и использовать глубокое обучение/нейронные сети для создания классификатора новостей с точностью человеческого уровня.
Хотя это довольно многословное руководство, «начать с нуля» здесь предполагает, что вы уже знакомы с основным синтаксисом Python.
Выберите источник данных для сканирования
Мы знаем, что при обучении с учителем предварительная обработка данных часто занимает больше времени, чем обучение модели. Поэтому очень важно найти источник данных, который организован и удобен для сканирования.
Давайте посмотрим на наше требование, то есть «учитывая заголовок новости, вернуть категорию новости», тогда каждый фрагмент данных, который мы собираем, должен иметь заголовок новости и категорию.
Среди новостных веб-сайтов, которые предоставляют исторические новости, прокручиваемая страница новостей China News Network (http://www.chinanews.com) должна быть самой легкой для сканирования:
Веб-ссылки, такие как:http://www.chinanews.com/scroll-news/2017/1224/news.shtml
Вы можете напрямую указать новости определенного дня, и страница сразу содержит все заголовки новостей и соответствующие категории дня.
Написать сканер данных
или ссылка вышеhttp://www.chinanews.com/scroll-news/2017/1224/news.shtml
Например, щелкните правой кнопкой мыши «Показать исходный код веб-страницы» на странице, и вы легко найдете заголовки новостей и категории, которые нам нужно просканировать:
<div class="content_list">
<ul>
<li><div class="dd_lm">[<a href=http://www.chinanews.com/world.shtml>国际</a>]</div> <div class="dd_bt"><a href="/gj/2017/12-24/8408109.shtml">天津交响乐团为尼泊尔带来新年音乐会</a></div><div class="dd_time">12-24 23:53</div></li>
<li><div class="dd_lm">[<a href=http://www.chinanews.com/wenhua.shtml>文化</a>]</div> <div class="dd_bt"><a href="/cul/2017/12-24/8408110.shtml">漫画“吾皇”系列作者白茶:走了心的作品才能走红</a></div><div class="dd_time">12-24 23:50</div></li>
省略一大波输出...
</ul>
</div>
скопировать код
Получить веб-контент
Я использую запросы здесь:
base_url = "http://www.chinanews.com/scroll-news/%s/%s/news.shtml" % (year, month_day)
resp = requests.get(base_url, timeout=10)
resp.encoding = "gbk"
скопировать код
Следует отметить, что метод кодирования этой веб-страницы — gbk, который необходимо явно указать с помощью resp.encoding, иначе это вызовет искаженные проблемы при последующем анализе.
Разбирать веб-страницы
Здесь мы воспользуемся знаменитым «прекрасным супом» BeautifulSoup. Конечно, его можно разобрать и с помощью регулярных выражений, но проще использовать bs вместо построения регулярных выражений, которые я не знаю, как придумать после нескольких дней накопления:
soup = BeautifulSoup(resp.text, "html.parser")
скопировать код
Сначала найдите сегмент div, содержащий данные, которые мы хотим получить.<div class="content_list"> ... </div>
:
content = soup.find('div', class_='content_list')
скопировать код
Затем найдите все li из div, то есть каждый содержит список заголовков новостей и категорий, например
<li><div class="dd_lm">[<a href=http://www.chinanews.com/world.shtml>国际</a>]</div> <div class="dd_bt"><a href="/gj/2017/12-24/8408109.shtml">天津交响乐团为尼泊尔带来新年音乐会</a></div><div class="dd_time">12-24 23:53</div></li>
скопировать код
li = content.find_all("li")
скопировать код
Каждый li содержит три элемента div: категория, заголовок новости и время выпуска. Здесь нам нужны только первые две части, но нам также нужно получить следующую ссылку в качестве ключа дедупликации:
category = item.find('div', class_='dd_lm').text.replace(r'[', '').replace(r']', '')
title = item.find('div', class_='dd_bt').text
href = item.find('div', class_='dd_bt').a.attrs['href']
скопировать код
хранилище данных
Я предпочитаю сохранять данные в словарь и хранить их в Redis, что удобно для последующей обработки с помощью Pandas.
di = {'category':category, 'title':title}
if not cli.hget('chinanews', href):
cli.hset('chinanews', href, str(di))
скопировать код
обработка времени
URL-адрес упомянутой выше страницы должен использовать информацию о годе, месяце и дне. Здесь я использую timedelta для расчета от текущего времени. Например, нижеследующее предназначено для захвата заголовков новостей и категорий за 1500 дней:
today = datetime.today()
for i in range(1, 1500):
whichday = (today - timedelta(days=i)).strftime("%Y-%m%d")
fetch_oneday(whichday)
time.sleep(2) # 做个有风度的爬虫
скопировать код
(Подробный код краулера смотрите по ссылке в конце статьи)
анализ данных
После примерно часа сбора у нас есть 2 299 879 почти 2,3 миллиона единиц данных: 1500 дней заголовков новостей и соответствующих категорий.
Перед предварительной обработкой данных мы должны выполнить предварительный анализ данных, чтобы удалить некоторые данные, такие как шум.
Чтение данных из Redis
cli = redis.Redis()
data = cli.hgetall('chinanews')
df = pd.DataFrame([ast.literal_eval(data[k]) for k in data])
скопировать код
Удалить бесполезные категории
categories = df.groupby('category').size()
pie = Pie("分类")
pie.add("", categories.index.tolist(), categories.values.tolist(), is_label_show=True, is_legend_show=False)
pie
скопировать код
Всего категорий 30, и видно, что распределение данных неравномерно.Сначала мы удаляем категории с данными менее 20 000, потому что небольшой объем данных все равно повлияет на точность модели; три категории "видео" и "отчеты" явно не различаются и могут быть только удалены:
categories = df.groupby('category').size()
categories = categories[categories > 20000].index.tolist()
# 设置 map 方法
filted = df['category'].map(lambda x: x in categories)
# 然后应用到 dataframe 上
df_filted = df[filted]
df_filted = df_filted[ (df_filted['category'] != u'图片') & (df_filted['category'] != u'视频') & (df_filted['category'] != u'报摘')]
скопировать код
Теперь у нас есть 2 178 902 элемента данных и 22 категории:
предварительная обработка данных
Теперь наши данные выглядят так:
Нейронные сети могут обрабатывать только числа, поэтому нам нужно соотнести категорию и заголовок с числами соответственно.
category
Преобразование классификации относительно просто.Вы можете отобразить все классификации из 1, сначала создать список классификаций, а затем создать два словаря, которые являются словарем имени классификации: номер и номер: имя классификации, что удобно для отображения и поиск:
catagories = df_filted.groupby('category').size().index.tolist()
catagory_dict = {}
int_catagory = {}
for i, k in enumerate(catagories):
catagory_dict.update({k:i})
int_catagory.update({i:k})
скопировать код
кадр данных плюс сопоставленный столбец, используйте метод применения:
df_filted['c2id'] = df_filted['category'].apply(lambda x: catagory_dict[x])
скопировать код
Таким образом, сопоставление категории с номером завершено, а столбцы title и c2id повторно выбираются для подготовки к следующей работе:
prepared_data = df_filted[ ['title', 'c2id'] ]
скопировать код
title
Заголовок новостей на самом деле является предложением. Нам нужно сопоставить предложение со списком чисел. Есть два способа:
Первый заключается в сопоставлении после сегментации слова, а другой — в непосредственном преобразовании и сопоставлении одного слова.
Преимущество первого заключается в высокой точности (при условии, что когда слово «Леброн» появляется в заголовке новости, в 99% случаев это, вероятно, спортивная новость, а если это одно слово «Ле», то, очевидно, больше). трудно классифицировать); недостатком является то, что при предсказании нового названия, если есть слово, которого нет в тезаурусе, его нельзя предсказать (самая распространенная проблема, такая как имя человека, не может быть сопоставлено и преобразовано, и эффект сегментации слов не очень хорош, если он отображается одним словом, такой проблемы нет).
Преимущества и недостатки последних прямо противоположны.
Поскольку нашей конечной целью является использование в качестве классификации новых данных, лучше использовать одно слово для карты преобразования.
prepared_data['words'] = prepared_data['title'].apply(lambda x: re.findall('[\x80-\xff]{3}|[\w\W]', x))
скопировать код
Обычный[\x80-\xff]{3}|[\w\W]
середина,[\x80-\xff]{3}
Настройка китайских иероглифов,[\w\W]
Настройте все остальные символы, такие как знаки препинания, пробелы и т. д.
Преобразованный результат:
шрифт
Создайте словарь сопоставлений для слов:
all_words = []
for w in prepared_data['words']:
all_words.extend(w)
word_dict = pd.DataFrame(pd.Series(all_words).value_counts())
word_dict['id'] = list(range(1, len(word_dict)+1))
скопировать код
Получаем словарь из 6790 «слов» (включая знаки препинания и пробелы и т. д.), в основном включающий все общеупотребительные слова, а соответствующие им id — разные числа.
Слова сопоставляются с числами
Добавьте новый столбец w2v для хранения преобразованной очереди чисел (выполнение занимает много времени):
prepared_data['w2v'] = prepared_data['words'].apply(lambda x: list(word_dict['id'][x]))
скопировать код
Затем завершите или усеките до очереди фиксированной длины из 25 (заголовки новостей обычно не превышают 25 слов):
maxlen = 25
prepared_data['w2v'] = list(sequence.pad_sequences(prepared_data['w2v'], maxlen=maxlen))
скопировать код
Окончательный подготовленный кадр данных:
Теперь наши данные X — это столбец w2v, а метка (цель) Y — столбец c2id.
Создание обучающих данных и тестовых данных
Просто используйте sklearn.model_selectiontrain_test_split
Случайным образом разделите все данные на обучающие и тестовые данные в соотношении 3:1:
seed = 7
X = np.array(list(prepared_data['w2v']))
Y = np.array(list(prepared_data['c2id']))
x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.25, random_state=seed)
скопировать код
Здесь более важно выполнить обработку to_categorical для Y, чтобы превратить Y в горячую форму.
Зачем переходить на одноразовый? Поскольку классификатор часто по умолчанию использует данные, данные являются непрерывными и упорядоченными. Но согласно нашему приведенному выше представлению, числа не упорядочены, а присваиваются случайным образом. При использовании one-hot эти функции являются взаимоисключающими и активируются только один раз за раз. Таким образом, данные становятся скудными.
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
скопировать код
LSTM-модель
Моделирование
Текстовая модель обычно использует модель RNN, Мы можем напрямую обратиться к официальному примеру LSTM Keras:
https://github.com/keras-team/keras/blob/master/examples/imdb_lstm.py
model = Sequential()
model.add(Embedding(len(word_dict)+1, 256))
model.add(LSTM(256))
model.add(Dropout(0.5))
model.add(Dense(y_train.shape[1]))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
скопировать код
В отличие от модели в примере, я добавил отсевающий слой посередине, чтобы избежать переобучения; кроме того, у нас мультиклассификация, поэтому на выходе окончательного полносвязного слоя должно быть количество категорий, функция активации изменена на softmax, а функция потерь изменена на categorical_crossentropy.
Повышение квалификации
model.fit(x_train, y_train, batch_size=128, epochs=20)
скопировать код
Из-за большого объема данных скорость обучения низкая.При использовании графического процессора GTX1080 раунд занимает около 10 минут.
После 10 эпох обучения точность падает медленнее, оставаясь на уровне около 74%.
Проверка данных испытаний
model.evaluate(x=x_test, y=y_test)
скопировать код
результат:
Точность на тестовом наборе составляет около 69%.
Протестируйте модель с новыми данными
Результаты немного неудовлетворительны, давайте проверим это на новых данных.
Новые данные необходимо обработать в список чисел так же, как и при предварительной обработке:
def predict_(title):
words = re.findall('[\x80-\xff]{3}|[\w\W]', title)
w2v = [word_dict[word_dict['0']==x]['id'].values[0] for x in words]
xn = sequence.pad_sequences([w2v], maxlen=maxlen)
predicted = model.predict_classes(xn, verbose=0)[0]
return int_catagory[predicted]
скопировать код
Затем потяните новые новости, чтобы проверить эффект:
Вот часть вывода:
Как видно из приведенных выше результатов, на самом деле, классификация некоторых заголовков новостей, результаты, оцененные сенсорной моделью, являются более точными, например:
[社会] prediction:[I T] 华为否认提前发年终奖 网传消息实为销售激励计划
[社会] prediction:[房产] 中国这个地方如同仙境 豪华别墅每平米仅1300元
[汽车] prediction:[I T] 阿里巴巴高管调整 蒋凡任淘宝总裁 靖捷任天猫总裁
[社会] prediction:[港澳] 香港青年学子“冬聚吉林”:多层面了解国家变化
скопировать код
Если первые три категории прогнозов считаются правильными:
def predict_3(title):
words = re.findall('[\x80-\xff]{3}|[\w\W]', title)
w2v = [word_dict[word_dict['0']==x]['id'].values[0] for x in words]
xn = sequence.pad_sequences([w2v], maxlen=maxlen)
predicted = model.predict(xn, verbose=0)[0]
predicted_sort = predicted.argsort()
li = [(int_catagory[p], predicted[p]*100) for p in predicted_sort[-3:]]
return li[::-1]
скопировать код
Видно, что вероятность успеха близка к 90%.
Исходя из этого результата, наша модель по-прежнему реализуема.
другие модели
GRU
model = Sequential()
model.add(Embedding(len(word_dict)+1, 256))
model.add(GRU(256))
model.add(Dense(y_train.shape[1]))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
скопировать код
Точность немного ниже, чем у LSTM (73,52% для обучающего набора/10 эпох; 67,83% для тестового набора);
Но время обучения меньше, около 480 с за раунд.
BiLSTM + CNN
embedding_size=128
hidden_size=256
model = Sequential()
model.add(Embedding(input_dim=len(word_dict)+1, output_dim=128, input_length=25))
model.add(Bidirectional(LSTM(256, return_sequences=True)))
model.add(TimeDistributed(Dense(64)))
model.add(Activation('softplus'))
model.add(MaxPooling1D(5))
model.add(Flatten())
model.add(Dense(y_train.shape[1]))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
скопировать код
Точность обучающего набора немного выше, чем у LSTM (76,55%/10 эпох), но точность тестового набора немного ниже (68,29%), а время обучения больше, требуя 1080 с/1 эпоху.
Сохранить модель и данные
В конце концов, была использована модель LSTM. Сохраните данные и модель для следующего вызова:
model.save('model.hdf5')
import pickle
def save_obj(obj, name ):
with open(name + '.pkl', 'wb') as f:
pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL)
save_obj(int_catagory, 'int_catagory')
save_obj(catagory_dict, 'catagory_dict')
word_dict.to_csv('word_dict.csv', encoding='utf8')
prepared_data.to_csv('prepared_data.csv', encoding='utf8')
скопировать код
заявление
Например, сервис API, аналогичный облачной платформе:
Используйте Flask для создания службы API, пользователь публикует заголовок новости и возвращает первые три возможные категории прогнозов:
➜ ~ curl -H "Content-Type: application/json" -X POST -d '{"title":"彭文生:房价下降才能促进宏观杠杆率的可持续下"}' http://192.168.15.24:5000/classify
{
"result": {
"1.category": "房产",
"1.possibility": "0.650669",
"2.category": "财经",
"2.possibility": "0.285769",
"3.category": "金融",
"3.possibility": "0.0191255"
}
}
➜ ~ curl -H "Content-Type: application/json" -X POST -d '{"title":"潘粤明后台自拍 扮相温文尔雅表情搞怪反差萌"}' http://192.168.15.24:5000/classify
{
"result": {
"1.category": "娱乐",
"1.possibility": "0.845201",
"2.category": "台湾",
"2.possibility": "0.108812",
"3.category": "港澳",
"3.possibility": "0.0148304"
}
}
скопировать код
Неплохо, теперь вы можете увольнять редакторов сайтов, которые только классифицируют новости.
Код и примечания Jupyter: https://github.com/jackhuntcn/news_category_classify
Have Fun!
Нерегулярно обновляйте оригинальные статьи о начальном уровне и ненадежном сборе данных, анализе данных, глубоком обучении и других интересных сценариях, пожалуйста, нажмите и удерживайте QR-код ниже, чтобы следовать👇