From 44fe496a2154b4c07cd078cf27d66bcb82ef3489 Mon Sep 17 00:00:00 2001 From: zhaoxi Date: Mon, 14 Sep 2026 08:49:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=86=E6=84=9F=E7=9F=A5?= =?UTF-8?q?=E6=9C=BA=E6=A8=A1=E5=9E=8B=EF=BC=8CK=E6=9C=80=E8=BF=91?= =?UTF-8?q?=E9=82=BB=E6=A8=A1=E5=9E=8B=EF=BC=8C=E6=9C=B4=E7=B4=A0=E8=B4=9D?= =?UTF-8?q?=E5=8F=B6=E6=96=AF=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + Cargo.lock | 89 +++++++++++ Cargo.toml | 7 + README.md | 8 + src/lib.rs | 6 + src/main.rs | 5 + src/model.rs | 5 + src/model/knn.rs | 1 + src/model/knn/knn.rs | 81 ++++++++++ src/model/model.rs | 19 +++ src/model/naive_bayes.rs | 4 + .../naive_bayes/bernoulli_naive_bayes.rs | 29 ++++ src/model/naive_bayes/gaussian_naive_bayes.rs | 34 ++++ src/model/naive_bayes/naive_bayes.rs | 136 ++++++++++++++++ src/model/naive_bayes/poisson_naive_bayes.rs | 27 ++++ src/model/perceptron.rs | 1 + src/model/perceptron/perceptron.rs | 76 +++++++++ src/model/utils.rs | 24 +++ tests/model_test.rs | 146 ++++++++++++++++++ 19 files changed, 700 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/model.rs create mode 100644 src/model/knn.rs create mode 100644 src/model/knn/knn.rs create mode 100644 src/model/model.rs create mode 100644 src/model/naive_bayes.rs create mode 100644 src/model/naive_bayes/bernoulli_naive_bayes.rs create mode 100644 src/model/naive_bayes/gaussian_naive_bayes.rs create mode 100644 src/model/naive_bayes/naive_bayes.rs create mode 100644 src/model/naive_bayes/poisson_naive_bayes.rs create mode 100644 src/model/perceptron.rs create mode 100644 src/model/perceptron/perceptron.rs create mode 100644 src/model/utils.rs create mode 100644 tests/model_test.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b39d31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +.idea/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6fd3b77 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,89 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "matrixmultiply" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "ml_learn" +version = "0.1.0" +dependencies = [ + "ndarray", +] + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1444bb9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ml_learn" +version = "0.1.0" +edition = "2024" + +[dependencies] +ndarray = "0.17.2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc9c5d1 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +一个练手的机器学习项目,目前包含以下几种模型: + +- perceptron(基础感知机模型) +- KNN(K最近邻模型) +- naive_bayes(朴素贝叶斯模型)包含以下几种: + - gaussian_naive_bayes(高斯朴素贝叶斯) + - poisson_naive_bayes(泊松朴素贝叶斯) + - berboulli_naive_bayes(伯努利朴素贝叶斯) \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5048a52 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod model; + +pub use model::model::{Model, TestDataSet, TestOutput, TrainDataset}; +pub use model::knn::knn::KNNModel; +pub use model::naive_bayes::naive_bayes::NaiveBayesClassifier; +pub use model::perceptron::perceptron::Perceptron; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ed1ffda --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ + + +fn main() { + println!("Hello, world!"); +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..84321da --- /dev/null +++ b/src/model.rs @@ -0,0 +1,5 @@ +pub mod perceptron; +mod utils; +pub mod knn; +pub mod naive_bayes; +pub mod model; diff --git a/src/model/knn.rs b/src/model/knn.rs new file mode 100644 index 0000000..7fb9d74 --- /dev/null +++ b/src/model/knn.rs @@ -0,0 +1 @@ +pub mod knn; diff --git a/src/model/knn/knn.rs b/src/model/knn/knn.rs new file mode 100644 index 0000000..f128303 --- /dev/null +++ b/src/model/knn/knn.rs @@ -0,0 +1,81 @@ +use std::collections::HashMap; +use crate::model::model::{Model, TestDataSet, TestOutput, TrainDataset}; +use crate::model::utils::l2_distance; + +pub struct KdTree; + +pub struct BaseModel { + k: usize, + input: Vec<(Vec, String)>, +} + +pub enum KNNModel { + KdTree(KdTree), + BaseKNN(BaseModel), +} + +impl KNNModel { + pub fn new(model_class: &str, k: usize) -> Self { + match model_class { + "base" => Self::BaseKNN(BaseModel { k, input: Vec::new() }), + _ => todo!() + } + } +} + +impl Model for KNNModel { + fn fit(&mut self, input: &TrainDataset) { + match self { + Self::BaseKNN(model) => { + // KNN 的"训练"只是把数据存下来 + model.input = input.input_data.rows() + .into_iter() + .map(|row| row.to_vec()) + .zip(input.output_data.iter().cloned()) + .collect(); + } + _ => todo!() + } + } + + fn predict(&self, input: &TestDataSet) -> TestOutput { + match self { + Self::BaseKNN(model) => { + let output_data = input.input_data.rows() + .into_iter() + .map(|row| { + let row = row.to_vec(); + // 1. 算所有距离 + let mut neighbors: Vec<(f64, &String)> = model.input.iter() + .map(|(x, label)| (l2_distance(x, &row), label)) + .collect(); + // 2. 按距离升序排(f64 用 partial_cmp 处理 NaN) + neighbors.sort_by(|a, b| { + a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal) + }); + // 3. 取前 k 个投票,找票数最多的 + let mut votes: HashMap<&String, usize> = HashMap::new(); + for (_, label) in neighbors.iter().take(model.k) { + *votes.entry(label).or_insert(0) += 1; + } + votes.into_iter() + .max_by_key(|&(_, count)| count) + .map(|(label, _)| label.clone()) + .unwrap_or_default() + }) + .collect(); + TestOutput { output_data } + } + _ => todo!() + } + } + + fn score(&self, input: &TrainDataset) -> f64 { + let prediction = self.predict(&TestDataSet { input_data: input.input_data.clone() }); + let correct = prediction.output_data.iter() + .zip(input.output_data.iter()) + .filter(|(p, t)| p == t) + .count(); + correct as f64 / input.output_data.len() as f64 + } +} diff --git a/src/model/model.rs b/src/model/model.rs new file mode 100644 index 0000000..c0b0906 --- /dev/null +++ b/src/model/model.rs @@ -0,0 +1,19 @@ +use ndarray::Array2; + +pub struct TrainDataset { + pub input_data: Array2, + pub output_data: Vec, +} + +pub struct TestDataSet { + pub input_data: Array2, +} + +pub struct TestOutput { + pub output_data: Vec, +} +pub trait Model { + fn fit(&mut self, input: &TrainDataset); + fn predict(&self, input: &TestDataSet) -> TestOutput; + fn score(&self, input: &TrainDataset) -> f64; +} \ No newline at end of file diff --git a/src/model/naive_bayes.rs b/src/model/naive_bayes.rs new file mode 100644 index 0000000..919989b --- /dev/null +++ b/src/model/naive_bayes.rs @@ -0,0 +1,4 @@ +pub mod gaussian_naive_bayes; +pub mod naive_bayes; +pub mod poisson_naive_bayes; +pub mod bernoulli_naive_bayes; \ No newline at end of file diff --git a/src/model/naive_bayes/bernoulli_naive_bayes.rs b/src/model/naive_bayes/bernoulli_naive_bayes.rs new file mode 100644 index 0000000..c973bee --- /dev/null +++ b/src/model/naive_bayes/bernoulli_naive_bayes.rs @@ -0,0 +1,29 @@ +use super::naive_bayes::NaiveBayes; +use ndarray::{ArrayView1, Array1}; + +pub struct BernoulliNaiveBayesLayer { + p: Array1, +} + +impl NaiveBayes for BernoulliNaiveBayesLayer { + fn fit(input: ArrayView1, output: &Vec, output_space_size: usize) -> Self { + let mut freq: Vec<(f64, usize)> = vec![(0.0, 0); output_space_size]; + for (i, o) in input.iter().zip(output.iter()) { + freq[*o].0 += i; + freq[*o].1 += 1; + } + let p: Array1 = freq.iter().map(|(sum, freq)| sum / *freq as f64).collect(); + Self { + p + } + } + fn predict(&self, input: f64) -> Array1 { + self.p.iter().map(|p| bernoulli_pmf(input, p)).collect() + } +} + +// 伯努利分布:input 只能取 0 或 1 +fn bernoulli_pmf(input: f64, p: &f64) -> f64 { + let k = input as i32; + p.powi(k) * (1.0 - p).powi(1 - k) +} diff --git a/src/model/naive_bayes/gaussian_naive_bayes.rs b/src/model/naive_bayes/gaussian_naive_bayes.rs new file mode 100644 index 0000000..503320d --- /dev/null +++ b/src/model/naive_bayes/gaussian_naive_bayes.rs @@ -0,0 +1,34 @@ +use super::naive_bayes::NaiveBayes; +use ndarray::{ArrayView1, Array1}; +pub struct GaussianNaiveBayesLayer { + mean_value: Array1, + variance: Array1, +} + +impl NaiveBayes for GaussianNaiveBayesLayer { + fn fit(input: ArrayView1, output: &Vec, output_space_size: usize) -> GaussianNaiveBayesLayer { + let mut mean_value: Vec<(f64, usize)> = vec![(0.0,0); output_space_size]; + for (i,o) in input.iter().zip(output.iter()){ + mean_value[*o].0 += i; + mean_value[*o].1 += 1; + } + let mean_value: Array1 = mean_value.iter().map(|(sum, freq)| sum / *freq as f64).collect(); + let mut variance: Vec<(f64, usize)> = vec![(0.0,0); output_space_size]; + for (i, o) in input.iter().zip(output.iter()) { + variance[*o].0 += (i - mean_value[*o]) * (i - mean_value[*o]); + variance[*o].1 += 1; + } + let variance: Array1 = variance.iter().map(|(sum, freq)| sum / *freq as f64).collect(); + Self { + mean_value, + variance + } + } + fn predict(&self, input: f64) -> Array1 { + self.mean_value.iter().zip(self.variance.iter()).map(|(m, v)| gaussian_pdf(input,m,v)).collect() + } +} +fn gaussian_pdf(input: f64, mean_value: &f64, variance: &f64) -> f64 { + let exp_part = - (input - mean_value).powi(2) / (2.0 * variance); + exp_part.exp() / (2.0 * std::f64::consts::PI * variance).sqrt() +} \ No newline at end of file diff --git a/src/model/naive_bayes/naive_bayes.rs b/src/model/naive_bayes/naive_bayes.rs new file mode 100644 index 0000000..fb82401 --- /dev/null +++ b/src/model/naive_bayes/naive_bayes.rs @@ -0,0 +1,136 @@ +use crate::model::model::{Model, TestOutput, TrainDataset, TestDataSet}; +use ndarray::{ArrayView1,Array1}; +use std::collections::HashMap; +use super::gaussian_naive_bayes::GaussianNaiveBayesLayer; +use super::poisson_naive_bayes::PoissonNaiveBayesLayer; +use super::bernoulli_naive_bayes::BernoulliNaiveBayesLayer; + +pub trait NaiveBayes { + fn fit(input: ArrayView1, output: &Vec, output_space_size: usize) -> Self; + fn predict(&self, input: f64) -> Array1; +} +pub enum NaiveBayesLayer { + GaussianNaiveBayes(Option), + PoissonNaiveBayes(Option), + BernoulliNaiveBayes(Option), +} + +impl NaiveBayesLayer { + fn predict(&self, input: f64) -> Array1 { + match self { + NaiveBayesLayer::GaussianNaiveBayes(Some(layer)) => layer.predict(input), + NaiveBayesLayer::GaussianNaiveBayes(None) => panic!("layer not fitted"), + NaiveBayesLayer::PoissonNaiveBayes(Some(layer)) => layer.predict(input), + NaiveBayesLayer::PoissonNaiveBayes(None) => panic!("layer not fitted"), + NaiveBayesLayer::BernoulliNaiveBayes(Some(layer)) => layer.predict(input), + NaiveBayesLayer::BernoulliNaiveBayes(None) => panic!("layer not fitted"), + } + } +} + +pub struct NaiveBayesClassifier{ + pub feature_size: usize, + pub feature_layer: Vec, + pub output_map: HashMap, + pub index_to_label: Vec, + pub prior_probability: Array1, +} + +impl NaiveBayesClassifier { + pub fn new() -> Self { + Self{ + feature_size: 0, + feature_layer: Vec::new(), + output_map: HashMap::new(), + index_to_label: Vec::new(), + prior_probability: Array1::::zeros(1) + } + } + pub fn create_classifier(&mut self, layer_names: Vec<&str>) { + self.feature_size = layer_names.len(); + for layer_name in layer_names { + match layer_name { + "GNB" => self.feature_layer.push(NaiveBayesLayer::GaussianNaiveBayes(None)), + "PoissonNB" => self.feature_layer.push(NaiveBayesLayer::PoissonNaiveBayes(None)), + "BernoulliNB" => self.feature_layer.push(NaiveBayesLayer::BernoulliNaiveBayes(None)), + _ => panic!("UNKNOWN LAYER NAME") + } + } + } +} + +impl Model for NaiveBayesClassifier { + fn fit(&mut self, input: &TrainDataset) { + let output = index_output(self, input); + let output_space_size = self.output_map.len(); + for i in 0..self.feature_size{ + let layer: NaiveBayesLayer = match self.feature_layer[i] { + NaiveBayesLayer::GaussianNaiveBayes(_) => { + NaiveBayesLayer::GaussianNaiveBayes(Some(GaussianNaiveBayesLayer::fit(input.input_data.column(i), &output, output_space_size))) + } + NaiveBayesLayer::PoissonNaiveBayes(_) => { + NaiveBayesLayer::PoissonNaiveBayes(Some(PoissonNaiveBayesLayer::fit(input.input_data.column(i), &output, output_space_size))) + } + NaiveBayesLayer::BernoulliNaiveBayes(_) => { + NaiveBayesLayer::BernoulliNaiveBayes(Some(BernoulliNaiveBayesLayer::fit(input.input_data.column(i), &output, output_space_size))) + } + }; + self.feature_layer[i] = layer; + } + } + fn predict(&self, input: &TestDataSet) -> TestOutput { + let mut output: Vec = Vec::with_capacity(input.input_data.nrows()); + for item in input.input_data.rows() { + let mut result: Array1 = self.prior_probability.clone(); + for i in 0..item.len(){ + result = result * self.feature_layer[i].predict(item[i]); + } + output.push(self.index_to_label[get_max(result)].clone()); + } + TestOutput { output_data: output } + } + fn score(&self, input: &TrainDataset) -> f64 { + let prediction = self.predict(&TestDataSet { input_data: input.input_data.clone() }); + let correct = prediction.output_data.iter() + .zip(input.output_data.iter()) + .filter(|(p, t)| p == t) + .count(); + correct as f64 / input.output_data.len() as f64 + } +} + +fn index_output(model: &mut NaiveBayesClassifier,dataset: &TrainDataset) -> Vec { + let mut statistics: HashMap = HashMap::new(); + let mut i: usize = 0; + let mut output: Vec = Vec::new(); + for sample in dataset.output_data.iter() { + match model.output_map.get(sample) { + Some(index) => output.push(*index), + None => { + model.output_map.insert(sample.to_string(),i); + model.index_to_label.push(sample.to_string()); + output.push(i); + i += 1; + } + } + *statistics.entry(sample.to_string()).or_insert(0) += 1; + } + let size = dataset.output_data.len(); + let mut prior_probability = vec![0.0; model.output_map.len()]; + for (key, freq) in &statistics { + if let Some(index) = model.output_map.get(key) { + prior_probability[*index] = *freq as f64 / size as f64; + } + } + model.prior_probability = Array1::::from(prior_probability); + output +} + +fn get_max(arr: Array1) -> usize { + let mut max: usize = 0; + for i in 1..arr.len( ){ + if arr[i] > arr[max] {max = i;} + } + max +} + diff --git a/src/model/naive_bayes/poisson_naive_bayes.rs b/src/model/naive_bayes/poisson_naive_bayes.rs new file mode 100644 index 0000000..59f818a --- /dev/null +++ b/src/model/naive_bayes/poisson_naive_bayes.rs @@ -0,0 +1,27 @@ +use super::naive_bayes::NaiveBayes; +use ndarray::{ArrayView1, Array1}; +use crate::model::utils::product; + +pub struct PoissonNaiveBayesLayer { + lambda: Array1, +} + +impl NaiveBayes for PoissonNaiveBayesLayer { + fn fit(input: ArrayView1, output: &Vec, output_space_size: usize) -> Self { + let mut lambda: Vec<(f64, usize)> = vec![(0.0,0); output_space_size]; + for (i,o) in input.iter().zip(output.iter()){ + lambda[*o].0 += i; + lambda[*o].1 += 1; + } + let lambda: Array1 = lambda.iter().map(|(sum, freq)| sum / *freq as f64).collect(); + Self { + lambda + } + } + fn predict(&self, input: f64) -> Array1 { + self.lambda.iter().map(|l| poisson_pmf(input, l)).collect() + } +} +fn poisson_pmf(input: f64, lambda: &f64) -> f64 { + lambda.powf(input) * (-lambda).exp() / product(input) +} diff --git a/src/model/perceptron.rs b/src/model/perceptron.rs new file mode 100644 index 0000000..892b8dd --- /dev/null +++ b/src/model/perceptron.rs @@ -0,0 +1 @@ +pub mod perceptron; diff --git a/src/model/perceptron/perceptron.rs b/src/model/perceptron/perceptron.rs new file mode 100644 index 0000000..721433f --- /dev/null +++ b/src/model/perceptron/perceptron.rs @@ -0,0 +1,76 @@ +use crate::model::model::{Model, TestDataSet, TestOutput, TrainDataset}; +use crate::model::utils::dot; + +pub struct Perceptron { + weight: Vec, + b: f64, + learning_rate: f64, + max_iterations: usize, + // 标签映射:labels[0] 视为正类(+1),labels[1] 视为负类(-1) + labels: Vec, +} + +impl Perceptron { + pub fn new(learning_rate: f64, max_iterations: usize) -> Self { + Self { + weight: Vec::new(), + b: 0.0, + learning_rate, + max_iterations, + labels: Vec::new(), + } + } +} + +impl Model for Perceptron { + fn fit(&mut self, input: &TrainDataset) { + self.weight = vec![0.0; input.input_data.ncols()]; + self.b = 0.0; + for label in &input.output_data { + if !self.labels.contains(label) { + self.labels.push(label.clone()); + } + } + assert_eq!(self.labels.len(), 2, "感知机只支持二分类"); + + let xs: Vec> = input.input_data.rows() + .into_iter() + .map(|row| row.to_vec()) + .collect(); + + for _ in 0..self.max_iterations { + let mut mistake = false; + for (x, label) in xs.iter().zip(input.output_data.iter()) { + let y = if *label == self.labels[0] { 1.0 } else { -1.0 }; + if (dot(&self.weight, x) + self.b) * y <= 0.0 { + for (w, xi) in self.weight.iter_mut().zip(x.iter()) { + *w += self.learning_rate * y * xi; + } + self.b += self.learning_rate * y; + mistake = true; + } + } + if !mistake { break; } + } + } + + fn predict(&self, input: &TestDataSet) -> TestOutput { + let output_data = input.input_data.rows() + .into_iter() + .map(|row| { + let result = dot(&self.weight, &row.to_vec()) + self.b; + if result >= 0.0 { self.labels[0].clone() } else { self.labels[1].clone() } + }) + .collect(); + TestOutput { output_data } + } + + fn score(&self, input: &TrainDataset) -> f64 { + let prediction = self.predict(&TestDataSet { input_data: input.input_data.clone() }); + let correct = prediction.output_data.iter() + .zip(input.output_data.iter()) + .filter(|(p, t)| p == t) + .count(); + correct as f64 / input.output_data.len() as f64 + } +} diff --git a/src/model/utils.rs b/src/model/utils.rs new file mode 100644 index 0000000..ec544d9 --- /dev/null +++ b/src/model/utils.rs @@ -0,0 +1,24 @@ +pub fn dot(a: &[f64], b: &[f64]) -> f64 { + assert_eq!(a.len(), b.len(), "长度不等"); + a.iter() + .zip(b.iter()) + .map(|(x, y)| x * y) + .sum() +} + +pub fn l2_distance(a: &[f64], b: &[f64]) -> f64 { + assert_eq!(a.len(), b.len(), "长度不等"); + a.iter() + .zip(b.iter()) + .map(|(x, y)| (x - y).powi(2)) + .sum::() + .sqrt() +} + +pub fn product(input: f64) -> f64 { + let mut prod: u64 = 1; + for i in 2..=input as u64{ + prod = prod * i; + } + prod as f64 +} \ No newline at end of file diff --git a/tests/model_test.rs b/tests/model_test.rs new file mode 100644 index 0000000..78420ce --- /dev/null +++ b/tests/model_test.rs @@ -0,0 +1,146 @@ +use ml_learn::{KNNModel, Model, NaiveBayesClassifier, Perceptron, TestDataSet, TrainDataset}; +use ndarray::Array2; + +#[test] +fn perceptron_fit_and_predict() { + // 线性可分数据:x1 + x2 > 2 为正类 + let input_data = Array2::from_shape_vec((6, 2), vec![ + 2.0, 2.0, 3.0, 1.0, 2.0, 3.0, // "pos" + 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // "neg" + ]).unwrap(); + let output_data = vec![ + "pos".to_string(), "pos".to_string(), "pos".to_string(), + "neg".to_string(), "neg".to_string(), "neg".to_string(), + ]; + let train = TrainDataset { input_data, output_data }; + + let mut model = Perceptron::new(1.0, 1000); + model.fit(&train); + assert_eq!(model.score(&train), 1.0); + + let test = TestDataSet { + input_data: Array2::from_shape_vec((2, 2), vec![4.0, 4.0, -1.0, 0.0]).unwrap(), + }; + let result = model.predict(&test); + assert_eq!(result.output_data, vec!["pos".to_string(), "neg".to_string()]); +} + +#[test] +fn knn_fit_and_predict() { + let input_data = Array2::from_shape_vec((6, 2), vec![ + 1.0, 1.0, 1.2, 0.8, 0.8, 1.2, // "A" + 5.0, 5.0, 5.2, 4.8, 4.8, 5.2, // "B" + ]).unwrap(); + let output_data = vec![ + "A".to_string(), "A".to_string(), "A".to_string(), + "B".to_string(), "B".to_string(), "B".to_string(), + ]; + let train = TrainDataset { input_data, output_data }; + + let mut model = KNNModel::new("base", 3); + model.fit(&train); + assert_eq!(model.score(&train), 1.0); + + let test = TestDataSet { + input_data: Array2::from_shape_vec((2, 2), vec![1.1, 0.9, 5.1, 5.1]).unwrap(), + }; + let result = model.predict(&test); + assert_eq!(result.output_data, vec!["A".to_string(), "B".to_string()]); +} + +#[test] +fn gaussian_naive_bayes_fit_and_predict() { + // 两个类别,单一连续特征,分布明显分开 + let input_data = Array2::from_shape_vec((6, 1), vec![ + 1.0, 1.2, 0.8, // "A" + 5.0, 5.2, 4.8, // "B" + ]).unwrap(); + let output_data = vec![ + "A".to_string(), "A".to_string(), "A".to_string(), + "B".to_string(), "B".to_string(), "B".to_string(), + ]; + let train = TrainDataset { input_data, output_data }; + + let mut clf = NaiveBayesClassifier::new(); + clf.create_classifier(vec!["GNB"]); + clf.fit(&train); + assert_eq!(clf.score(&train), 1.0); + + let test = TestDataSet { + input_data: Array2::from_shape_vec((2, 1), vec![1.1, 5.1]).unwrap(), + }; + let result = clf.predict(&test); + assert_eq!(result.output_data, vec!["A".to_string(), "B".to_string()]); +} + +#[test] +fn poisson_naive_bayes_fit_and_predict() { + // 计数特征:A 类 λ≈1,B 类 λ≈5 + let input_data = Array2::from_shape_vec((6, 1), vec![ + 0.0, 1.0, 2.0, // "A" + 4.0, 5.0, 6.0, // "B" + ]).unwrap(); + let output_data = vec![ + "A".to_string(), "A".to_string(), "A".to_string(), + "B".to_string(), "B".to_string(), "B".to_string(), + ]; + let train = TrainDataset { input_data, output_data }; + + let mut clf = NaiveBayesClassifier::new(); + clf.create_classifier(vec!["PoissonNB"]); + clf.fit(&train); + assert_eq!(clf.score(&train), 1.0); + + let test = TestDataSet { + input_data: Array2::from_shape_vec((2, 1), vec![1.0, 5.0]).unwrap(), + }; + let result = clf.predict(&test); + assert_eq!(result.output_data, vec!["A".to_string(), "B".to_string()]); +} + +#[test] +fn bernoulli_naive_bayes_fit_and_predict() { + // 0/1 特征:A 类大概率为 1,B 类大概率为 0 + let input_data = Array2::from_shape_vec((6, 1), vec![ + 1.0, 1.0, 1.0, // "A" + 0.0, 0.0, 0.0, // "B" + ]).unwrap(); + let output_data = vec![ + "A".to_string(), "A".to_string(), "A".to_string(), + "B".to_string(), "B".to_string(), "B".to_string(), + ]; + let train = TrainDataset { input_data, output_data }; + + let mut clf = NaiveBayesClassifier::new(); + clf.create_classifier(vec!["BernoulliNB"]); + clf.fit(&train); + + let test = TestDataSet { + input_data: Array2::from_shape_vec((2, 1), vec![1.0, 0.0]).unwrap(), + }; + let result = clf.predict(&test); + assert_eq!(result.output_data, vec!["A".to_string(), "B".to_string()]); +} + +#[test] +fn naive_bayes_mixed_layers() { + // 两个特征列分别用不同层:连续特征 GNB + 计数特征 PoissonNB + let input_data = Array2::from_shape_vec((6, 2), vec![ + 1.0, 0.0, + 1.2, 1.0, + 0.8, 2.0, // "A" + 5.0, 4.0, + 5.2, 5.0, + 4.8, 6.0, // "B" + ]).unwrap(); + let output_data = vec![ + "A".to_string(), "A".to_string(), "A".to_string(), + "B".to_string(), "B".to_string(), "B".to_string(), + ]; + let train = TrainDataset { input_data, output_data }; + + let mut clf = NaiveBayesClassifier::new(); + clf.create_classifier(vec!["GNB", "PoissonNB"]); + clf.fit(&train); + assert_eq!(clf.score(&train), 1.0); +}