Move cleaning transformers into classifiers/cleaning.py (dropping the separate cleaning package) and implement them as plain classes rather than BaseEstimator/TransformerMixin subclasses, since Pipeline only needs fit/transform via duck typing. Also rename feature_classifier.py and bag_of_words.py to features.py and bow.py for brevity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
44 lines
1.4 KiB
Python
44 lines
1.4 KiB
Python
import numpy as np
|
|
from sklearn.feature_extraction import DictVectorizer
|
|
from sklearn.linear_model import LogisticRegression
|
|
from sklearn.pipeline import Pipeline
|
|
|
|
|
|
class FeatureExtractor:
|
|
def fit(self, X, y=None):
|
|
return self
|
|
|
|
def transform(self, X):
|
|
return [self.extract_features(msg) for msg in X]
|
|
|
|
def extract_features(self, message):
|
|
return {
|
|
"contains_free": int("free" in message.lower()),
|
|
"num_exclamations": message.count("!"),
|
|
"length": len(message),
|
|
}
|
|
|
|
|
|
class FeatureClassifier:
|
|
def fit(self, X, y):
|
|
self._pipeline = Pipeline([
|
|
("features", FeatureExtractor()),
|
|
("vectorizer", DictVectorizer()),
|
|
("classifier", LogisticRegression(max_iter=1000)),
|
|
])
|
|
y_binary = (np.array(y) == "spam").astype(int)
|
|
self._pipeline.fit(X, y_binary)
|
|
return self
|
|
|
|
def predict(self, X):
|
|
y_binary = self._pipeline.predict(X)
|
|
return np.where(y_binary == 1, "spam", "ham")
|
|
|
|
def feature_weights(self, top_n=10):
|
|
vectorizer = self._pipeline.named_steps["vectorizer"]
|
|
classifier = self._pipeline.named_steps["classifier"]
|
|
names = vectorizer.get_feature_names_out()
|
|
weights = classifier.coef_[0]
|
|
pairs = sorted(zip(names, weights), key=lambda x: abs(x[1]), reverse=True)
|
|
return pairs[:top_n]
|